From 4a911043a130fa552e1f39b1e182f7b5d3f5a3cc Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:56:19 +0900 Subject: [PATCH 1/7] docs: add per-API Lua standard library reference Document every named Lua-facing standard-library API, table, and path environment setting with links to the official Lua 5.2 manual. Explain managed runtime differences, provider-dependent behavior, unavailable features, and current compatibility limitations with their mechanisms. Link the reference from both READMEs and correct the stale string.dump support note in the string-mode guide. Validate official anchors, registered API coverage, local links, and representative behavior against the implementation and native Lua 5.2. --- README.md | 5 +-- README_JA.md | 5 +-- docs/stdlib/README.md | 76 ++++++++++++++++++++++++++++++++++++++++ docs/stdlib/basic.md | 36 +++++++++++++++++++ docs/stdlib/bit32.md | 24 +++++++++++++ docs/stdlib/coroutine.md | 16 +++++++++ docs/stdlib/debug.md | 32 +++++++++++++++++ docs/stdlib/io.md | 56 +++++++++++++++++++++++++++++ docs/stdlib/math.md | 41 ++++++++++++++++++++++ docs/stdlib/os.md | 35 ++++++++++++++++++ docs/stdlib/package.md | 32 +++++++++++++++++ docs/stdlib/string.md | 44 +++++++++++++++++++++++ docs/stdlib/table.md | 18 ++++++++++ docs/string-modes.md | 2 +- 14 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 docs/stdlib/README.md create mode 100644 docs/stdlib/basic.md create mode 100644 docs/stdlib/bit32.md create mode 100644 docs/stdlib/coroutine.md create mode 100644 docs/stdlib/debug.md create mode 100644 docs/stdlib/io.md create mode 100644 docs/stdlib/math.md create mode 100644 docs/stdlib/os.md create mode 100644 docs/stdlib/package.md create mode 100644 docs/stdlib/string.md create mode 100644 docs/stdlib/table.md diff --git a/README.md b/README.md index 4119cd7b..537dea8b 100644 --- a/README.md +++ b/README.md @@ -161,10 +161,7 @@ var results = await state.DoStringAsync("return math.pi"); Console.WriteLine(results[0]); // 3.141592653589793 ``` -For more details on standard libraries, refer to the [Lua official manual](https://www.lua.org/manual/5.2/manual.html#6). - -> [!WARNING] -> Lua-CSharp does not support all functions of the standard libraries. For details, refer to the [Compatibility](#compatibility) section. +See the [standard library API reference](docs/stdlib/README.md) for each function's inputs/results, official Lua 5.2 links, and implementation differences with their reasons. It also lists unavailable APIs and current limitations. The [Lua official manual](https://www.lua.org/manual/5.2/manual.html#6) defines the full language/library contract. ## Functions diff --git a/README_JA.md b/README_JA.md index 4aa75c47..988ee086 100644 --- a/README_JA.md +++ b/README_JA.md @@ -161,10 +161,7 @@ var results = await state.DoStringAsync("return math.pi"); Console.WriteLine(results[0]); // 3.141592653589793 ``` -標準ライブラリについては[Lua公式のマニュアル](https://www.lua.org/manual/5.2/manual.html#6)を参照してください。 - -> [!WARNING] -> Lua-CSharpは標準ライブラリの全ての関数をサポートしているわけではありません。詳細は[互換性](#互換性)の項目を参照してください。 +各APIの引数・戻り値、Lua 5.2公式リファレンスへのリンク、実装上の違いと理由は[標準ライブラリAPIリファレンス(英語)](docs/stdlib/README.md)を参照してください。未対応APIと現在の制限も記載しています。言語・ライブラリの完全な仕様は[Lua公式のマニュアル](https://www.lua.org/manual/5.2/manual.html#6)を参照してください。 ## 関数 diff --git a/docs/stdlib/README.md b/docs/stdlib/README.md new file mode 100644 index 00000000..a24d98c6 --- /dev/null +++ b/docs/stdlib/README.md @@ -0,0 +1,76 @@ +# Lua standard library reference + +This reference describes the Lua-facing APIs installed by Lua-CSharp. Each API links to its entry in the [official Lua 5.2 manual](https://www.lua.org/manual/5.2/manual.html#6). The manual defines the Lua contract; these pages explain the managed implementation, available configuration, and known limitations. + +## Open libraries + +```csharp +using Lua; +using Lua.Platforms; +using Lua.Standard; + +using var state = LuaState.Create(); +state.OpenStandardLibraries(); + +// For Lua byte strings and raw stream contents: +using var bytes = LuaState.Create(LuaPlatform.DefaultByteStrings with +{ + NumberFormat = LuaNumberFormat.Lua, +}); +bytes.OpenStandardLibraries(); +``` + +Libraries are opt-in. `LuaState.Create()` alone does not install them. To install a subset, call the corresponding extension in the table below. Configure standard I/O before opening its library: changing the provider later does not replace existing handles. Existing module closures also retain their captured package table. + +| Reference | C# initialization | Official library/global | +| --- | --- | --- | +| [Basic functions and globals](basic.md) | `OpenBasicLibrary()` | [Basic library](https://www.lua.org/manual/5.2/manual.html#6.1) | +| [Coroutines](coroutine.md) | `OpenCoroutineLibrary()` | [`coroutine`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine) | +| [Packages and modules](package.md) | `OpenModuleLibrary()` | [`package`](https://www.lua.org/manual/5.2/manual.html#pdf-package) | +| [Strings](string.md) | `OpenStringLibrary()` | [`string`](https://www.lua.org/manual/5.2/manual.html#pdf-string) | +| [Tables](table.md) | `OpenTableLibrary()` | [`table`](https://www.lua.org/manual/5.2/manual.html#pdf-table) | +| [Mathematics](math.md) | `OpenMathLibrary()` | [`math`](https://www.lua.org/manual/5.2/manual.html#pdf-math) | +| [Bitwise operations](bit32.md) | `OpenBitwiseLibrary()` | [`bit32`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32) | +| [Files and standard I/O](io.md) | `OpenIOLibrary()` | [`io`](https://www.lua.org/manual/5.2/manual.html#pdf-io) | +| [Operating system](os.md) | `OpenOperatingSystemLibrary()` | [`os`](https://www.lua.org/manual/5.2/manual.html#pdf-os) | +| [Debugging](debug.md) | `OpenDebugLibrary()` | [`debug`](https://www.lua.org/manual/5.2/manual.html#pdf-debug) | + +## Reading the API tables + +Signatures use `[optional]` arguments and `...` for multiple values. A blank differences cell means no additional difference is documented for that API; the shared behavior below still applies. These are implementation notes, not a guarantee that every edge case matches every C Lua build. + +Notes marked **Limitation** describe incomplete compatibility, not a recommended Lua idiom. Notes marked **Host-dependent** depend on a replaceable provider. Explanations identify the implementation mechanism; they do not imply that an existing limitation is intentional. + +## Shared behavior + +### Strings and positions + +The default `LuaStringMode.Utf16` uses .NET UTF-16 code units. `LuaStringMode.Bytes` stores one byte per C# `char`, matching Lua 5.2's byte-string representation. This affects lengths, slices, pattern positions, character classes, and stream counts. Neither mode counts grapheme clusters. See [String modes](../string-modes.md) for conversions and stream configuration. + +This difference allows ordinary C# strings to be used directly in the default mode. Choose byte mode for Lua scripts that depend on byte positions or binary file contents. The `b` in `io.open` does not select the state's string mode. + +### Numbers and text + +Lua numbers use .NET `double`. `LuaPlatform.NumberFormat` defaults to `DotNet`, using invariant .NET numeric text. `Lua` selects 14 significant digits for implicit conversions and portable signed `inf`/`nan` spellings. This is a formatting policy; it does not change arithmetic or parsing. + +This setting affects numeric `tostring`, `print`, numeric I/O, `table.concat`, `gsub` replacements, and applicable `string.format` conversions. A particular C runtime may spell special values differently, including Windows `nan(ind)` or `nan(snan)`; the `Lua` policy deliberately uses portable spellings. See [number formatting](../../README.md#number-formatting). + +### Async calls, cancellation, and errors + +C# functions and platform providers can await asynchronous work. Lua execution awaits them; an incomplete `Task` is not a Lua `coroutine.yield`. Lua callbacks entered through non-yieldable library operations, such as a sort comparator or a `gsub` replacement, cannot yield across that boundary. This preserves Lua 5.2 behavior while allowing C# async integration. + +Host execution cancellation propagates to C# rather than becoming a normal `pcall` result. Built-in stream reads check cancellation at entry and before buffer fills; cancellation cannot interrupt an already blocking synchronous `Stream.Read`. Error wording and tracebacks can contain .NET exception messages and C# frames. + +### Memory and host objects + +The .NET garbage collector owns object lifetimes. Lua GC tuning, weak-table `__mode` semantics, and Lua `__gc` finalizers are not implemented as a separate Lua collector. `collectgarbage` has significant limitations documented in [Basic functions](basic.md). Close files explicitly when their close timing matters. + +C# objects can be exposed as userdata, and host functions are wrapped as `LuaFunction` values. Their identity strings and debug metadata describe managed objects rather than native pointers. Host-frozen tables, including generated C# metatables, reject mutation even through raw table operations; this keeps their published interop definitions immutable. + +Metatable-bearing host userdata implements `ILuaUserData`. Light-userdata object tokens have no metatable slot in the current managed metatable dispatch, so `debug.setmetatable` does not attach one to those tokens. + +## Scope + +The pages include missing APIs from Lua 5.2, not just registered functions. Legacy Lua 5.1 compatibility aliases and later-version additions are listed separately where useful. This reference covers Lua script APIs; it does not replace the C# API documentation. + +Implementation entry point: [OpenLibsExtensions.cs](../../src/Lua/Standard/OpenLibsExtensions.cs). diff --git a/docs/stdlib/basic.md b/docs/stdlib/basic.md new file mode 100644 index 00000000..ad421d41 --- /dev/null +++ b/docs/stdlib/basic.md @@ -0,0 +1,36 @@ +# Basic functions and globals + +[Reference index](README.md) · [Official Lua 5.2 basic library](https://www.lua.org/manual/5.2/manual.html#6.1) · [Implementation](../../src/Lua/Standard/BasicLibrary.cs) + +Install with `state.OpenBasicLibrary()`. Functions are globals. The [shared string, numeric-text, cancellation, and host-object behavior](README.md#shared-behavior) applies throughout. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`assert(v [, message])`](https://www.lua.org/manual/5.2/manual.html#pdf-assert) | Returns all arguments when `v` is truthy; otherwise raises an assertion error. The default message is `assertion failed!`. | The host receives a `LuaAssertionException`. Lua 5.2's message is string-like: a number is converted to text; an arbitrary table is not an assertion error object. | +| [`collectgarbage([opt [, arg]])`](https://www.lua.org/manual/5.2/manual.html#pdf-collectgarbage) | Recognizes `collect`, `stop`, `restart`, `count`, `step`, `setpause`, `setstepmul`, `setmajorinc`, `isrunning`, `incremental`, and `generational`. | **Limitation:** every recognized option calls `GC.Collect()` and returns **no values**. `arg` is ignored. Thus `count` does not report memory, `stop` does not stop GC, and tuning/mode options do not tune it. The implementation delegates to .NET GC without implementing Lua's collector control protocol. | +| [`dofile([filename])`](https://www.lua.org/manual/5.2/manual.html#pdf-dofile) | Loads and executes a file; returns its results and raises load/runtime failures. | **Limitation:** a filename is required; omitted/nil input does not load standard input. Loading goes through `LuaPlatform.FileSystem`. Paths are decoded to .NET text in byte mode. | +| [`error(message [, level])`](https://www.lua.org/manual/5.2/manual.html#pdf-error) | Raises the supplied error object. `level` selects the source prefix for string errors; `0` suppresses that prefix. | Host-facing errors use `LuaRuntimeException`; stack descriptions include managed frames. | +| [`_G`](https://www.lua.org/manual/5.2/manual.html#pdf-_G) | The initial global environment table. | Installed from the state's managed environment. Assigning `_G` does not rewrite a function's `_ENV`. | +| [`getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-getmetatable) | Returns the metatable, its protected `__metatable` value, or nil. | Also works with managed userdata. | +| [`ipairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-ipairs) | Uses `__ipairs` when present; otherwise returns an iterator, the table, and `0`, stopping at the first missing integer element. | **Limitation:** directly calling the default iterator with a fractional control value increments that double instead of first converting it to a C integer. At exhaustion it returns two nils rather than one. The ordinary generic-for sequence use is unaffected. `__ipairs` cannot yield across the library callback boundary. | +| [`load(ld [, source [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-load) | Compiles source/bytecode from a string or a reader function. Returns a function, or `nil, error`; an empty reader result ends input. `mode` defaults to `bt`. Explicit `env`, including nil, initializes the first upvalue. | **Limitation:** reader pieces are collected before parsing, so readers can be called past the point where C Lua would detect a syntax error or finish a binary chunk. This follows the current whole-input compiler interface. Reader callbacks cannot Lua-yield, but C# readers can await. Raw binary strings work in either mode; see [dumping](string.md#dumping-and-loading). | +| [`loadfile([filename [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-loadfile) | Compiles a file without running it; returns a function or `nil, error`. Recognizes an initial UTF-8 BOM and first-line `#` comment. | **Limitation:** filename is required; there is no standard-input overload in the Lua library. Uses the configured filesystem, with byte-mode paths decoded to .NET text. | +| [`next(t [, index])`](https://www.lua.org/manual/5.2/manual.html#pdf-next) | Returns the next key/value, or nil after the last key. Omitted/nil `index` starts traversal. | Order follows the managed table storage and is unspecified. Do not depend on matching C Lua's hash iteration order. | +| [`pairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-pairs) | Uses `__pairs`, or returns the default iterator, table, and nil. | `__pairs` cannot Lua-yield across the library callback boundary. | +| [`pcall(f [, ...])`](https://www.lua.org/manual/5.2/manual.html#pdf-pcall) | Returns `true, ...results`, or `false, errorObject`. Callable values with `__call` are supported. | Host execution cancellation escapes to C# rather than being consumed as a script error. | +| [`print(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-print) | Calls the global `tostring` for each argument and writes tab-separated output plus a newline. | **Host-dependent:** writes to `LuaPlatform.StandardIO.Output`, not the file selected by `io.output`. The output provider may be a console, redirected stream, or Unity logger. Numeric text uses `NumberFormat`; the `tostring` callbacks cannot Lua-yield. | +| [`rawequal(a, b)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawequal) | Compares values without `__eq`. | Managed object identity applies to reference-backed values. | +| [`rawget(t, key)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawget) | Reads a table slot without `__index`. | | +| [`rawlen(v)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawlen) | Returns the raw table length or string length without `__len`. | String length counts UTF-16 code units in default mode and bytes in byte mode. | +| [`rawset(t, key, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawset) | Writes without `__newindex` and returns `t`. | Host-frozen tables still reject writes; raw access bypasses metamethods, not the managed table's read-only state. | +| [`select(index, ...)`](https://www.lua.org/manual/5.2/manual.html#pdf-select) | `"#"` returns the argument count. A numeric index selects a suffix; negative indices count from the end. | | +| [`setmetatable(t, mt)`](https://www.lua.org/manual/5.2/manual.html#pdf-setmetatable) | Assigns a table metatable or removes it with nil; returns `t`. Protected metatables cannot be changed. | Host-frozen tables also prevent replacing their metatable. | +| [`tonumber(v [, base])`](https://www.lua.org/manual/5.2/manual.html#pdf-tonumber) | Parses numeric text or returns nil. With an explicit base, accepts bases 2–36 and a signed integer string. | A numeric `v` with an explicit base is first formatted using `NumberFormat`, which can affect parsing. Explicit-base digits accumulate in a double, as in the default C Lua 5.2 implementation; no newer Lua integer-subtype arithmetic is used. | +| [`tostring(v)`](https://www.lua.org/manual/5.2/manual.html#pdf-tostring) | Uses `__tostring` when present; otherwise produces the value's standard text. | Numeric output uses `NumberFormat`; managed identities replace C pointer-like representations. `__tostring` cannot Lua-yield through this callback. | +| [`type(v)`](https://www.lua.org/manual/5.2/manual.html#pdf-type) | Reports a Lua type name, including `userdata` for host objects and `thread` for Lua states. | C# functions are reported as `function`; .NET types are not exposed as separate Lua type names. | +| [`_VERSION`](https://www.lua.org/manual/5.2/manual.html#pdf-_VERSION) | The string `"Lua 5.2"`. | Reports the language target, not the Lua-CSharp NuGet/package version. | +| [`xpcall(f, msgh [, ...])`](https://www.lua.org/manual/5.2/manual.html#pdf-xpcall) | Calls `f` protected; on failure invokes `msgh(error)` before unwinding and returns `false, handledError`. On success returns `true, ...results`. | A handler failure produces `error in error handling`. The implementation terminates handler failure directly instead of repeatedly re-entering the same handler. Host execution cancellation propagates to C#. | + +## Compatibility names + +Lua-CSharp does not install the deprecated Lua 5.1 names `loadstring`, global `unpack`, `getfenv`, `setfenv`, or `module`. Use `load`, `table.unpack`, and Lua 5.2 `_ENV`-based modules. See the [official library changes](https://www.lua.org/manual/5.2/manual.html#8.2). diff --git a/docs/stdlib/bit32.md b/docs/stdlib/bit32.md new file mode 100644 index 00000000..1e306454 --- /dev/null +++ b/docs/stdlib/bit32.md @@ -0,0 +1,24 @@ +# Bitwise operations + +[Reference index](README.md) · [Official Lua 5.2 bit32 library](https://www.lua.org/manual/5.2/manual.html#6.7) · [Implementation](../../src/Lua/Standard/BitwiseLibrary.cs) + +Install with `state.OpenBitwiseLibrary()`. Results are doubles representing unsigned 32-bit words, not a separate integer type. + +Word arguments are rounded with .NET `Math.Round` (nearest, ties to even) and reduced modulo 2^32. Lua 5.2 permits implementation-dependent conversion of fractional words; this is Lua-CSharp's choice, matching the usual IEEE-754 Lua 5.2 conversion. Shift amounts, field positions, and widths are converted separately by truncating to managed integers. Use integral arguments when matching results across implementations. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`bit32.arshift(x, disp)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.arshift) | Arithmetic right shift with sign extension; negative displacement shifts left. | Uses signed 32-bit shifting internally and returns the unsigned word. | +| [`bit32.band(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.band) | Bitwise AND; no arguments returns all 32 bits set. | | +| [`bit32.bnot(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.bnot) | Inverts all 32 bits. | | +| [`bit32.bor(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.bor) | Bitwise OR; no arguments returns zero. | | +| [`bit32.btest(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.btest) | Reports whether the AND of its arguments is nonzero. | | +| [`bit32.bxor(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.bxor) | Bitwise XOR; no arguments returns zero. | | +| [`bit32.extract(n, field [, width])`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.extract) | Extracts a field at bit offset `field`, with width defaulting to 1. | Uses managed integer field/width conversion and rejects fields outside the 32-bit word. | +| [`bit32.lrotate(x, disp)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.lrotate) | Rotates left; negative displacement rotates right. | Rotation displacement is masked modulo 32. | +| [`bit32.lshift(x, disp)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.lshift) | Logical left shift; negative displacement shifts right. Magnitudes at least 32 produce zero. | | +| [`bit32.replace(n, v, field [, width])`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.replace) | Replaces the selected field using the low bits of `v`; default width is 1. | Uses managed integer field/width conversion and checks the 32-bit field boundary. | +| [`bit32.rrotate(x, disp)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.rrotate) | Rotates right; negative displacement rotates left. | Rotation displacement is masked modulo 32. | +| [`bit32.rshift(x, disp)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.rshift) | Logical right shift with zero fill; negative displacement shifts left. Magnitudes at least 32 produce zero. | | + +These library calls provide Lua 5.2 bit operations. Lua 5.3 integer bitwise operators and 64-bit integer-subtype behavior are outside this API's contract. diff --git a/docs/stdlib/coroutine.md b/docs/stdlib/coroutine.md new file mode 100644 index 00000000..73c17a66 --- /dev/null +++ b/docs/stdlib/coroutine.md @@ -0,0 +1,16 @@ +# Coroutines + +[Reference index](README.md) · [Official Lua 5.2 coroutine library](https://www.lua.org/manual/5.2/manual.html#6.2) · [Implementation](../../src/Lua/Standard/CoroutineLibrary.cs) + +Install with `state.OpenCoroutineLibrary()`. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`coroutine.create(f)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.create) | Creates a suspended coroutine whose body is `f`; returns a `thread`. | Also accepts a C# `LuaFunction` body, since host functions participate in the same managed call machinery. | +| [`coroutine.resume(co [, ...])`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.resume) | Starts/resumes `co`; returns `true` followed by yielded or final results, or `false, error` for a script failure. Resume arguments become body arguments or yield results. | Can await asynchronous C# work. Host cancellation follows the managed execution cancellation protocol rather than becoming an ordinary script failure. | +| [`coroutine.running()`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.running) | Returns the current thread and a boolean identifying the main thread. | Threads are represented by `LuaState` objects. They are not dedicated OS threads. | +| [`coroutine.status(co)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.status) | Reports `running`, `suspended`, `normal`, or `dead`. | Awaiting C# work does not itself create a Lua yield/resume exchange. | +| [`coroutine.wrap(f)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.wrap) | Returns a resume function that forwards results and raises script failures instead of returning a success boolean. | The wrapper is a managed closure capturing the coroutine. Its capture is visible to the supported debug upvalue APIs. | +| [`coroutine.yield(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.yield) | Suspends the current coroutine and passes values to its resumer. Later resume arguments become the yield's results. | Main-thread yields and yields across non-yieldable library callbacks fail. C# `await` is separate: an async host callback can await without being allowed to call Lua `yield`. | + +See [async and cancellation](README.md#async-calls-cancellation-and-errors) and the [C# coroutine guide](../../README.md#coroutines). No Lua 5.3/5.4 additions such as `coroutine.isyieldable` or `coroutine.close` are installed; this library targets Lua 5.2. diff --git a/docs/stdlib/debug.md b/docs/stdlib/debug.md new file mode 100644 index 00000000..3f63c86a --- /dev/null +++ b/docs/stdlib/debug.md @@ -0,0 +1,32 @@ +# Debugging + +[Reference index](README.md) · [Official Lua 5.2 debug library](https://www.lua.org/manual/5.2/manual.html#6.10) · [Implementation](../../src/Lua/Standard/DebugLibrary.cs) + +Install with `state.OpenDebugLibrary()`. Thread-aware functions accept an optional first `thread`; otherwise they inspect the current state. Metadata describes Lua bytecode and managed call frames, not a native Lua C stack. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`debug.debug()`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.debug) | Officially enters an interactive debug command loop. | **Unavailable:** not registered. Interactive console/debugger integration must be provided by the host. | +| [`debug.gethook([thread])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.gethook) | Returns the installed hook, mask, and count. With no hook: nil, empty mask, and zero count. | A host-installed hook is a managed `LuaFunction`, not a native C hook pointer. Its execution policy can differ from a Lua-installed hook; see below. | +| [`debug.getinfo([thread,] f [, what])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getinfo) | Returns function/frame metadata selected by `n`, `S`, `l`, `u`, `t`, `f`, and `L`, or nil for a missing stack level. | C# functions report `what="C#"` and `source="=[C#]"` instead of C-function labels. `u` exposes managed closure capture counts. **Limitation:** numeric strings are not accepted as stack levels, because the function/level dispatch tests the Lua value tag before numeric coercion. | +| [`debug.getlocal([thread,] f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getlocal) | Inspects a frame local/temporary/vararg, or a Lua function's parameter name; returns nil when no such local exists. | Managed frames expose their Lua-facing argument/temporary slots, not arbitrary C# local variables. The implementation operates on Lua-CSharp stack metadata. | +| [`debug.getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getmetatable) | Returns the actual metatable, bypassing `__metatable` protection. | May return a host-frozen generated metatable. Reading the actual table does not make its contents writable. | +| [`debug.getregistry()`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getregistry) | Returns the state's registry table. | Registry contents are managed runtime state; C API integer-slot conventions and native pointers are not a portable contract for Lua-CSharp. | +| [`debug.getupvalue(f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getupvalue) | Returns an upvalue name/value, or no values for an invalid slot. | Supports Lua closures and `CSharpClosure.UpValues`. Managed capture names are empty strings. **Limitation:** stripped Lua upvalue names also remain empty instead of C Lua's `(no name)` placeholder, because missing serialized names are stored as empty strings. | +| [`debug.getuservalue(u)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getuservalue) | Reads a userdata's first user-value slot, or nil when absent. | Uses `ILuaUserData.UserValues`. Host userdata can expose no slots; values are `LuaValue` entries rather than Lua 5.2's table/nil-only uservalue model. | +| [`debug.sethook([thread,] hook, mask [, count])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.sethook) | Installs/removes a hook; mask letters are `c`, `r`, and `l`, with an optional instruction count. | Lua-installed hooks cannot Lua-yield. They can call C# code that awaits. Counting is based on VM instructions, not time spent inside a C# function. See [hook details](#hook-details). | +| [`debug.setlocal([thread,] level, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setlocal) | Writes a frame local and returns its name, or nil when unavailable. | Writes Lua-facing managed stack slots; it cannot set arbitrary CLR locals. | +| [`debug.setmetatable(value, mt)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setmetatable) | Replaces/removes the actual metatable, bypassing `__metatable` protection; returns the value. | Host-frozen tables still reject changing their metatable because `LuaTable.Freeze()` protects managed storage. **Limitation:** light-userdata object tokens have no case in the managed metatable dispatcher, so setting their metatable has no effect. | +| [`debug.setupvalue(f, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setupvalue) | Writes an upvalue and returns its name, or no values for an invalid slot. | Can update managed closure capture slots; their names are empty. Missing stripped Lua names have the same placeholder limitation as `getupvalue`. | +| [`debug.setuservalue(u, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setuservalue) | Writes the first user-value slot and returns the userdata. | **Difference/limitation:** any Lua value is accepted, rather than Lua 5.2's table/nil restriction. Userdata exposing no `UserValues` storage returns nil. This reflects the host-defined `Span` storage API. | +| [`debug.traceback([thread,] [message [, level]])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.traceback) | Builds a traceback, passing non-string/non-number error objects through unchanged. | Formats managed/C# frames and Lua source metadata rather than reproducing C Lua's exact trace text. **Limitations:** explicit negative levels return nil; supplying the current thread explicitly defaults to level 0 instead of C Lua's current-thread level 1. Numeric messages use the managed value's `ToString`, independently of `NumberFormat`. These follow the current formatter/default-level implementation. | +| [`debug.upvalueid(f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.upvalueid) | Returns an identity token for an upvalue slot; joined Lua slots share an identity. | Uses weakly associated managed identity objects instead of a native address. Retaining the token does not retain the captured payload; this fits managed GC without exposing movable CLR storage addresses. | +| [`debug.upvaluejoin(f1, n1, f2, n2)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.upvaluejoin) | Makes one Lua closure's slot share another Lua closure's upvalue. | Restricted to Lua closures, as in Lua 5.2. Managed capture arrays can be inspected/updated but are not joined by this API. | + +## Hook details + +Hook callbacks receive an event string and a line number or nil. Events include `call`, `return`, `tail call`, `line`, and `count`. Count hooks measure executed Lua instructions; they do not tick while execution is waiting inside a C# function. Nested Lua execution from C# can execute instructions and trigger hooks. + +Hooks installed from C# through `LuaState.SetHook` retain the host hook policy, including supported async/yield behavior; they are distinguished internally from `debug.sethook` callbacks. This lets host instrumentation integrate with async execution while keeping Lua-installed callbacks compatible with non-yieldable Lua 5.2 hooks. See the [C# debug-hook guide](../../README.md#debug-hooks). + +Stripped bytecode has no line/local names to recover. Source/line fields and local lookup reflect the metadata actually present; dumping and reloading a stripped chunk cannot reconstruct it. **Limitation:** `getinfo` can report an unknown Lua line as `0` instead of C Lua's `-1`, because the managed line lookup uses zero when line info is absent. diff --git a/docs/stdlib/io.md b/docs/stdlib/io.md new file mode 100644 index 00000000..eca25969 --- /dev/null +++ b/docs/stdlib/io.md @@ -0,0 +1,56 @@ +# Files and standard I/O + +[Reference index](README.md) · [Official Lua 5.2 I/O library](https://www.lua.org/manual/5.2/manual.html#6.8) · [Library implementation](../../src/Lua/Standard/IOLibrary.cs) · [File methods](../../src/Lua/Standard/FileHandle.cs) + +Install with `state.OpenIOLibrary()`. Operations use `LuaPlatform.FileSystem` and the standard-stream handles captured from `LuaPlatform.StandardIO` when the library opens. A custom provider can use memory, assets, or another storage system. + +## Library APIs + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`io.close([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.close) | Closes the supplied file, or the default output when no argument is supplied. Explicit nil is an error. | Default console standard handles reject closing. File failures use managed messages/error codes; see below. | +| [`io.flush()`](https://www.lua.org/manual/5.2/manual.html#pdf-io.flush) | Flushes the current default output. | Calls the configured stream's flush operation; it is not necessarily an OS file stream. | +| [`io.input([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.input) | Gets the default input, selects an open handle, or opens a filename for reading and selects it. | **Host-dependent:** filenames go through the filesystem provider. Omitted/nil input is a getter; invalid/closed handles are rejected before changing the default. | +| [`io.lines([filename, ...formats])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.lines) | Iterates using read formats. A filename creates an automatically closed file; omitted/nil filename uses default input without automatic close. Default format is `*l`. | **Limitation:** at EOF the iterator returns a nil result instead of zero results. Generic-for termination is unchanged, but `select('#', iterator())` can differ. This shares the managed read helper's result convention. Closed default input is rejected when creating the iterator. | +| [`io.open(filename [, mode])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.open) | Opens a file, defaulting to `r`; returns a handle or `nil, message, code` for handled open failures. | String mode, rather than `b`, determines the built-in stream's byte/text encoding. Errors include .NET messages/HResult codes. Custom providers can supply different storage semantics. | +| [`io.output([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.output) | Gets/selects default output, or opens a filename for writing and selects it. | Switching output flushes the previous open output in this implementation. This is an additional effect of its explicit managed flush policy. Invalid/closed arguments are validated before that flush. It does not redirect `print`, which uses `StandardIO.Output` directly. | +| [`io.popen(command [, mode])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.popen) | Officially opens a process pipe. | **Unavailable:** not registered; the built-in I/O abstraction does not implement process pipes. A host can expose its own process/stream binding. | +| [`io.read(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-io.read) | Reads from the selected default input using the formats below. | Shares `file:read` semantics, including string units and provider error behavior. | +| [`io.stderr`](https://www.lua.org/manual/5.2/manual.html#pdf-io.stderr) | Initial standard error handle. | **Host-dependent:** wraps the configured error stream; default console handles cannot be closed through Lua. | +| [`io.stdin`](https://www.lua.org/manual/5.2/manual.html#pdf-io.stdin) | Initial standard input handle. | **Host-dependent:** wraps the configured input stream. Selecting another `io.input` does not replace this field. | +| [`io.stdout`](https://www.lua.org/manual/5.2/manual.html#pdf-io.stdout) | Initial standard output handle. | **Host-dependent:** wraps the configured output stream. Selecting another `io.output` does not replace this field. | +| [`io.tmpfile()`](https://www.lua.org/manual/5.2/manual.html#pdf-io.tmpfile) | Creates a temporary read/write file handle. | The default provider uses a .NET `FileStream` with `DeleteOnClose`. **Limitation:** creation failures are raised rather than converted to Lua's usual nil/error result; this entry directly awaits the provider. Close explicitly for deterministic deletion. | +| [`io.type(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-io.type) | Returns `file`, `closed file`, or nil. | Recognizes managed `FileHandle` objects and queries their provider's open state. | +| [`io.write(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-io.write) | Writes string/number arguments to default output and returns that file on success. | Numbers use `NumberFormat`; strings use the configured stream encoding. | + +## File methods + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`file:close()`](https://www.lua.org/manual/5.2/manual.html#pdf-file:close) | Closes the underlying stream; reports success or handled I/O failure. | Calls `ILuaStream.CloseAsync`; close behavior belongs to the provider. The default stream flushes and disposes the underlying .NET stream. Lua GC finalizers are not used to implement deterministic close. | +| [`file:flush()`](https://www.lua.org/manual/5.2/manual.html#pdf-file:flush) | Flushes pending writes. | Calls the provider's flush operation. | +| [`file:lines(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:lines) | Returns an iterator over the file using the given formats; does not automatically close the handle. | Closed handles fail at iterator creation. **Limitation:** the EOF result count follows the same nil-versus-no-results difference as `io.lines`. | +| [`file:read(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:read) | Returns one value per successful format, stopping at the first failed conversion/EOF with nil. No formats means `*l`. | Default streams decode UTF-8 to UTF-16, while byte-mode streams preserve raw bytes. Counts use the selected representation. Numeric scanning remains byte-based, even in UTF-16 mode. | +| [`file:seek([whence [, offset]])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:seek) | Seeks using `set`, `cur`, or `end`; defaults are `cur, 0`. Returns the resulting position or handled I/O failure. | Offsets/positions are **bytes** in built-in streams, including UTF-16 mode. Buffered unread bytes are accounted for. If a counted read returned only the high surrogate of a supplementary character, its bytes have already been consumed; a position query retains the pending low surrogate. Underlying seek support is provider-dependent. | +| [`file:setvbuf(mode [, size])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:setvbuf) | Selects `no`, `line`, or `full` buffering policy; returns true when accepted. | **Limitation:** built-in `LuaStream` changes flush policy, not the underlying .NET buffer allocation. `no` flushes each write; `line` flushes writes containing LF; `full` uses a per-write size threshold rather than a cumulative C stdio buffer. `size` does not resize the stream's buffer. Custom providers may implement their own policy. | +| [`file:write(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:write) | Writes strings/numbers and returns the file on success. | Default streams encode UTF-16 text as UTF-8; byte-mode streams write raw byte values. Numbers use `NumberFormat`. | + +## Formats, modes, and errors + +The supported read formats are `*n`, `*a`, `*l`, `*L`, and a numeric count. The format dispatcher also accepts `*number`, `*all`, and `*line`. Bare `L` is an additional accepted alias for `*L`; use `*L` for the Lua 5.2 spelling. `*a` returns an empty string at EOF; a count of zero probes EOF without consuming a character. Numeric counts are truncated and must fit a nonnegative managed `int`. + +With built-in streams: + +| Operation | Default UTF-16 mode | Byte mode | +| --- | --- | --- | +| `read(n)` | Up to `n` UTF-16 units; supplementary characters can be split across results | Up to `n` raw bytes | +| `read('*l')` | CR, LF, and CRLF end a line; the ending is removed | LF ends a line; a preceding CR remains in the result | +| `read('*L')` | Keeps the original line ending | Keeps LF and any preceding CR | +| Initial UTF-8 BOM in a text read | Skipped only at the beginning | Preserved as data | +| Invalid UTF-8 | Replaced when decoded | Preserved as bytes | + +`*n` scans decimal/hex numeric bytes and consumes incomplete numeric input items when a conversion fails. It does not decode Unicode whitespace or skip a UTF-8 BOM as a Unicode text read would. This is the numeric scanner's implementation boundary. + +Recognized open modes are `r`, `rb`, `r+`, `r+b`, `w`, `wb`, `w+`, `w+b`, `a`, `ab`, `a+`, and `a+b`. The `b` marker does not change string mode: built-in `LuaStream` encoding was selected by its constructor/platform. Use a matching byte-mode platform for binary files, including `string.dump` output. + +Handled failures generally return `nil, message, code`; closed-file misuse raises an error. Managed error codes are exception `HResult` values, not C `errno` values. Some provider exceptions propagate as Lua errors instead of becoming a result tuple. See [shared cancellation behavior](README.md#async-calls-cancellation-and-errors). diff --git a/docs/stdlib/math.md b/docs/stdlib/math.md new file mode 100644 index 00000000..f44160a1 --- /dev/null +++ b/docs/stdlib/math.md @@ -0,0 +1,41 @@ +# Mathematics + +[Reference index](README.md) · [Official Lua 5.2 math library](https://www.lua.org/manual/5.2/manual.html#6.6) · [Implementation](../../src/Lua/Standard/MathematicsLibrary.cs) + +Install with `state.OpenMathLibrary()`. Numbers are .NET doubles. Most operations call `System.Math`; decomposition/scaling use managed helpers. Transcendental rounding and exceptional-value details can vary with the .NET runtime/platform, just as C math libraries vary between Lua builds. No C math library or mutable C locale is used. + +Angles are radians unless an API explicitly converts degrees. The [numeric text policy](README.md#numbers-and-text) affects printing results, not the calculations. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`math.abs(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.abs) | Absolute value. | `Math.Abs`. | +| [`math.acos(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.acos) | Inverse cosine. | `Math.Acos`; managed floating-point behavior applies. | +| [`math.asin(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.asin) | Inverse sine. | `Math.Asin`. | +| [`math.atan(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.atan) | Inverse tangent. | One argument, as in Lua 5.2; use `atan2` for two coordinates. | +| [`math.atan2(y, x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.atan2) | Quadrant-aware inverse tangent for `(y, x)`. | `Math.Atan2`. | +| [`math.ceil(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.ceil) | Smallest integral double at least `x`. | | +| [`math.cos(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.cos) | Cosine. | `Math.Cos`. | +| [`math.cosh(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.cosh) | Hyperbolic cosine. | `Math.Cosh`. | +| [`math.deg(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.deg) | Radians to degrees. | Multiplies by `180 / Math.PI`. | +| [`math.exp(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.exp) | Exponential with base e. | `Math.Exp`. | +| [`math.floor(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.floor) | Largest integral double at most `x`. | | +| [`math.fmod(x, y)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.fmod) | Remainder using a quotient truncated toward zero. | Uses the C# double remainder operator. This differs from Lua's `%` operator for some negative inputs, as `fmod` should. | +| [`math.frexp(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.frexp) | Returns mantissa and binary exponent, including subnormal decomposition. | Implemented by a managed double-bit helper rather than C `frexp`. | +| [`math.huge`](https://www.lua.org/manual/5.2/manual.html#pdf-math.huge) | Positive infinity. | Backed by `double.PositiveInfinity`. | +| [`math.ldexp(m, e)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.ldexp) | Scales `m` by a power of two; exponent is converted to an integer. | Uses `Math.ScaleB` on newer targets and a managed fallback on .NET Standard 2.1. | +| [`math.log(x [, base])`](https://www.lua.org/manual/5.2/manual.html#pdf-math.log) | Natural log, or log in the specified base. | Uses `Math.Log`, a base-10 specialization, or the ratio of natural logs. | +| [`math.max(x, ...)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.max) | Maximum of at least one numeric argument. | Uses ordered comparisons, preserving Lua 5.2's argument-order behavior for NaN/equal values. | +| [`math.min(x, ...)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.min) | Minimum of at least one numeric argument. | Uses ordered comparisons rather than a different NaN-selecting aggregate. | +| [`math.modf(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.modf) | Returns integral and fractional parts. | Managed helper preserves signed zero and handles infinities. | +| [`math.pi`](https://www.lua.org/manual/5.2/manual.html#pdf-math.pi) | The double approximation of pi. | Backed by `Math.PI`. | +| [`math.pow(x, y)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.pow) | `x` raised to `y`. | `Math.Pow`. | +| [`math.rad(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.rad) | Degrees to radians. | Multiplies by `Math.PI / 180`. | +| [`math.random([m [, n]])`](https://www.lua.org/manual/5.2/manual.html#pdf-math.random) | Without bounds, a value in `[0,1)`. With bounds, uses Lua 5.2's floor-based interval formula. | Uses `System.Random`; sequences differ from C Lua's C-runtime generator. For fractional bounds, the Lua 5.2 formula is retained; integer-only checks from newer Lua versions are not imposed. The generator is stored in the shared global environment, so coroutines in that state share it. | +| [`math.randomseed(seed)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.randomseed) | Replaces the generator using the converted seed; returns no values. | Uses a 32-bit seed and `System.Random`. Repeatability is tied to the .NET generator/runtime, not C Lua's `srand`/`rand` sequence. | +| [`math.sin(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.sin) | Sine. | `Math.Sin`. | +| [`math.sinh(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.sinh) | Hyperbolic sine. | `Math.Sinh`. | +| [`math.sqrt(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.sqrt) | Square root. | `Math.Sqrt`. | +| [`math.tan(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.tan) | Tangent. | `Math.Tan`. | +| [`math.tanh(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.tanh) | Hyperbolic tangent. | `Math.Tanh`. | + +The deprecated `math.log10` alias is absent; use `math.log(x, 10)`. See [official library changes](https://www.lua.org/manual/5.2/manual.html#8.2). Integer-subtype APIs such as `math.type`, `math.tointeger`, and `math.maxinteger` belong to later Lua versions and are not installed. diff --git a/docs/stdlib/os.md b/docs/stdlib/os.md new file mode 100644 index 00000000..7cf26ebc --- /dev/null +++ b/docs/stdlib/os.md @@ -0,0 +1,35 @@ +# Operating system + +[Reference index](README.md) · [Official Lua 5.2 OS library](https://www.lua.org/manual/5.2/manual.html#6.9) · [Implementation](../../src/Lua/Standard/OperatingSystemLibrary.cs) + +Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process exit, and filesystem operations are routed through `LuaPlatform` providers where indicated. Default providers use .NET APIs; Unity/custom providers can behave differently. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`os.clock()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.clock) | Returns elapsed CPU time in seconds. | **Host-dependent:** calls `ILuaOsEnvironment.GetTotalProcessorTime`. The default reads the current .NET process's total processor time, including host work outside Lua. | +| [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. | Uses `DateTime`, `TimeProvider`, and invariant .NET formatting rather than C `strftime`. **Limitation:** several conversions are incomplete or differ; see below. | +| [`os.difftime(t2, t1)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns `t2 - t1` in seconds. | Direct double subtraction, matching the Unix-seconds representation used here. | +| [`os.execute([command])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.execute) | Officially runs a shell command or queries shell availability. | **Unsupported operation:** no command/nil returns false; supplying a command raises a not-supported error. The library has no shell-execution implementation. | +| [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitation:** the `close` argument is ignored, so this API does not explicitly dispose the Lua state before exiting. | +| [`os.getenv(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.getenv) | Returns an environment value or nil. | **Host-dependent:** default provider reads process environment variables; custom/Unity providers may expose a managed dictionary instead. Names/results are converted between byte strings and .NET text when needed. | +| [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a path or reports failure. | **Limitation:** default `FileSystem.Remove` removes files only, even on POSIX hosts where C Lua can remove empty directories. It uses `File.Exists`/`File.Delete` and has no directory-removal path. Handled errors use .NET HResult codes. | +| [`os.rename(oldname, newname)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.rename) | Moves/renames a file or directory; reports success or handled failure. | **Host-dependent:** default provider uses `File.Move`/`File.Replace` and `Directory.Move`. Directory replacement restrictions follow .NET/host filesystem behavior rather than C `rename`; .NET Standard 2.1 uses `File.Replace` for an existing file destination. | +| [`os.setlocale(locale [, category])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Officially selects or queries a C locale. | **Limitation:** only the literal `C` is accepted; other strings return nil, nil is not a locale-query operation, and `category` is ignored. No process C locale is changed; the implementation uses invariant managed formatting and mode-specific string rules. | +| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. | **Limitation:** constructs a .NET `DateTime`, so out-of-range fields are rejected rather than normalized by C `mktime`. `isdst` is ignored and the input table is not rewritten with normalized fields. Default current time comes from `TimeProvider`. | +| [`os.tmpname()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.tmpname) | Returns a temporary pathname. | **Host-dependent:** default `Path.GetTempFileName` creates an empty file while choosing the name. The caller owns cleanup; unlike `io.tmpfile`, this API does not attach delete-on-close behavior. The name uses the state's string representation. | + +## Date conversion details + +The date formatter is a managed mapping, not a complete C `strftime` implementation. The following conversions are recognized: + +`%a %A %b %B %c %C %d %D %e %F %g %G %h %H %I %j %m %M %n %p %r %R %S %t %T %u %w %x %X %y %Y %z %Z %%` + +Known formatting differences and incomplete conversions include: + +- `%U`, `%V`, and `%W` currently emit the literal `??`; week-number calculation is not implemented. +- `%g`/`%G` use the calendar year, not the ISO week-based year defined for those conversions by POSIX `strftime`. Some C Lua builds reject these conversions entirely. +- `%z` and `%Z` both use .NET's `zzz` offset format, including a colon, rather than separate C numeric-offset/timezone-name conversions. +- `%c`, `%x`, `%X`, month/day names, and related patterns use invariant .NET formatting; changing `os.setlocale` has no effect. +- `%E`/`%O` modifiers and unknown conversions raise an error. + +Timestamp conversion is bounded by .NET date/time ranges. For byte-string states, format literals are represented as byte-string units; the formatter's generated names come from invariant .NET date strings. Use the official link for the complete Lua/C contract and this list for the implemented behavior. diff --git a/docs/stdlib/package.md b/docs/stdlib/package.md new file mode 100644 index 00000000..b62075f1 --- /dev/null +++ b/docs/stdlib/package.md @@ -0,0 +1,32 @@ +# Packages and modules + +[Reference index](README.md) · [Official Lua 5.2 package library](https://www.lua.org/manual/5.2/manual.html#6.3) · [Implementation](../../src/Lua/Standard/ModuleLibrary.cs) + +Install with `state.OpenModuleLibrary()`. This installs global `require` and the `package` table. Each opened standard library is registered in the loaded-module table. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`require(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-require) | Returns a truthy cached module or searches for a loader. Calls the loader with `name` and searcher data; caches a non-nil result, or uses the loaded entry/default `true`. A false cache entry allows a later reload. | **Host-dependent:** a configured `ILuaModuleLoader` is consulted before package searchers when it reports the module exists. This lets C# supply resources/assets without native filesystem loading. The package object is captured when the library is opened, so replacing global `package` does not retarget an existing `require`. Loader/searcher callbacks cannot Lua-yield, but host loaders can await. | +| [`package.config`](https://www.lua.org/manual/5.2/manual.html#pdf-package.config) | A newline-delimited string describing directory, path-list, substitution, executable-directory, and ignore markers. | The directory character is the host .NET separator; the remaining markers are `;`, `?`, `!`, and `-`. This is initialized from the host runtime, not a C Lua build configuration. | +| [`package.cpath`](https://www.lua.org/manual/5.2/manual.html#pdf-package.cpath) | Officially configures native C module search paths. | **Unavailable:** no field or native C-module searcher is installed. Lua-CSharp uses managed bindings and host module loaders rather than Lua's C ABI. | +| [`package.loaded`](https://www.lua.org/manual/5.2/manual.html#pdf-package.loaded) | The table used to cache module values. | Backed by the state's managed loaded-module table. As in Lua 5.2, replacing the `package.loaded` field does not replace the cache used by `require`; mutate its entries instead. | +| [`package.loadlib(libname, funcname)`](https://www.lua.org/manual/5.2/manual.html#pdf-package.loadlib) | Officially loads a native library and its Lua C entry point. | **Unavailable:** not registered. A native Lua C function cannot run directly against Lua-CSharp's managed state/stack representation. Expose C# functions or an `ILuaModuleLoader` instead. | +| [`package.path`](https://www.lua.org/manual/5.2/manual.html#pdf-package.path) | A semicolon-separated list of Lua file templates used by the Lua searcher; `?` is replaced with the module path. | Starts as `?.lua`. The library does not initialize it from `LUA_PATH`, versioned environment variables, or an executable installation directory. Configure it explicitly or supply a host module loader. | +| [`package.preload`](https://www.lua.org/manual/5.2/manual.html#pdf-package.preload) | Maps module names to preloaded loader functions, used by searcher 1. | Stored in the managed preload table. Preload entries may be Lua or C# functions. | +| [`package.searchers`](https://www.lua.org/manual/5.2/manual.html#pdf-package.searchers) | Ordered searcher functions returning a loader and optional data, or an error-description string. A missing integer entry ends the search. | Initially contains only preload and Lua-file searchers, in that order. Native C and C-root searchers are absent because there is no native C-module loader. Applications may add Lua/C# searchers. | +| [`package.searchpath(name, path [, sep [, rep]])`](https://www.lua.org/manual/5.2/manual.html#pdf-package.searchpath) | Replaces `sep` (default `.`) in the name with `rep`, tries templates in `path`, and returns the matching filename or `nil, diagnostics`. | **Host-dependent:** readability is decided by `ILuaFileSystem.IsReadable`; the default uses `File.Exists`, not an actual open attempt. Default `rep` comes from the filesystem provider's directory separator. | + +## Host configuration + +Paths returned to Lua use the state's string representation. Paths passed to filesystem/module providers are .NET text, decoded from byte strings where necessary. See [module loading from C#](../../README.md#module-loading). + +The default package initializer does not consult the C Lua path environment variables: + +| Official environment setting | Lua-CSharp behavior and reason | +| --- | --- | +| [`LUA_PATH_5_2`](https://www.lua.org/manual/5.2/manual.html#pdf-LUA_PATH_5_2) | Not read; `package.path` starts as `?.lua` and is configured by Lua/C# code. | +| [`LUA_PATH`](https://www.lua.org/manual/5.2/manual.html#pdf-LUA_PATH) | Not used as a fallback; the initializer has no environment-path expansion step. | +| [`LUA_CPATH_5_2`](https://www.lua.org/manual/5.2/manual.html#pdf-LUA_CPATH_5_2) | Not read; native C-module search/loading is absent. | +| [`LUA_CPATH`](https://www.lua.org/manual/5.2/manual.html#pdf-LUA_CPATH) | Not read for the same native-loader limitation. | + +The deprecated `module`/`package.seeall` compatibility helpers and `package.loaders` alias are not installed. Use Lua 5.2 module tables and `package.searchers`; see [official library changes](https://www.lua.org/manual/5.2/manual.html#8.2). diff --git a/docs/stdlib/string.md b/docs/stdlib/string.md new file mode 100644 index 00000000..c7e567cf --- /dev/null +++ b/docs/stdlib/string.md @@ -0,0 +1,44 @@ +# Strings + +[Reference index](README.md) · [Official Lua 5.2 string library](https://www.lua.org/manual/5.2/manual.html#6.4) · [Implementation](../../src/Lua/Standard/StringLibrary.cs) + +Install with `state.OpenStringLibrary()`. String method calls such as `s:sub(2)` use this library through the string metatable. + +The central difference is representation: operations use UTF-16 code units by default and bytes in `LuaStringMode.Bytes`. This makes default-mode operations convenient for .NET strings; byte mode preserves Lua's binary-string model. See [String modes](../string-modes.md). Indices are 1-based; negative substring indices count from the end. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`string.byte(s [, i [, j]])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.byte) | Returns the numeric units in the inclusive range, defaulting to the first unit. | Default mode returns UTF-16 code-unit values, including surrogate values. Byte mode returns 0–255. The managed string representation determines the units. | +| [`string.char(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.char) | Constructs a string from numeric character values. | Byte mode requires 0–255, as Lua byte strings do. Default mode casts the integer values to UTF-16 `char`, rather than enforcing a byte range; use ordinary text or byte mode according to the intended data. | +| [`string.dump(f)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.dump) | Serializes a Lua function's prototype as a raw binary string. Captured values are not serialized. C# functions cannot be dumped. | Available in both string modes; the result always holds raw byte values. See [dumping and loading](#dumping-and-loading). | +| [`string.find(s, pattern [, init [, plain]])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.find) | Returns match start/end and captures, or nil. `plain` selects literal substring search. | Positions use the state's string units. Pattern character classes use the rules below; plain search uses ordinal string matching. | +| [`string.format(format, ...)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.format) | Formats values using Lua's printf-style conversions, including `%s`, `%q`, integer, floating-point, and hexadecimal floating-point conversions. | Width/precision count managed string units. `%c` constructs a UTF-16 unit in default mode and masks to one byte in byte mode. Numeric text and special values follow `NumberFormat` where applicable. Integer formatting uses 64-bit signed/unsigned ranges; these can differ from a C Lua build's configured integer formatting width. | +| [`string.gmatch(s, pattern)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.gmatch) | Returns an iterator over captures, or whole matches when there are no captures. | Positions and classes follow the selected string mode. | +| [`string.gsub(s, pattern, replacement [, n])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.gsub) | Returns the replaced string and match count. Replacement may be text, a number, a table, or a function. False/nil replacement results retain the matched text. | Numeric replacements use `NumberFormat`. Table `__index` and replacement callbacks can await C# work but cannot Lua-yield through this library call. | +| [`string.len(s)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.len) | Returns the string length, including embedded zero units. | Counts UTF-16 units by default and bytes in byte mode; neither counts Unicode grapheme clusters. | +| [`string.lower(s)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.lower) | Converts uppercase units to lowercase. | Default mode uses .NET current-culture Unicode casing through `String.ToLower`; byte mode changes ASCII A–Z only. Native Lua uses its C locale. `os.setlocale` does not set .NET's current culture. | +| [`string.match(s, pattern [, init])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.match) | Returns captures or the whole match, or nil. | Positions and classes follow the selected string mode. | +| [`string.rep(s, n [, sep])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.rep) | Repeats `s`, inserting `sep` between copies. Non-positive counts produce an empty string. | Counts are truncated as in Lua 5.2; resulting lengths are bounded by .NET string/int capacity. | +| [`string.reverse(s)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.reverse) | Reverses the sequence of string units. | Default mode can split/reverse surrogate pairs; byte mode reverses bytes and can split UTF-8 sequences. This operation is not Unicode text-element reversal. | +| [`string.sub(s, i [, j])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.sub) | Returns the inclusive slice, with `j` defaulting to the last unit. | Slices UTF-16 units or bytes according to the mode, including partial surrogate pairs/UTF-8 sequences. | +| [`string.upper(s)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.upper) | Converts lowercase units to uppercase. | Default mode uses .NET current-culture Unicode casing through `String.ToUpper`; byte mode changes ASCII a–z only. The host's .NET culture, rather than the process C locale, controls default-mode casing. | + +## Patterns + +[Official pattern syntax](https://www.lua.org/manual/5.2/manual.html#6.4.1) applies: Lua patterns are not .NET regular expressions. Captures, position captures `()`, balanced matches `%b`, frontier patterns `%f`, anchors, and Lua repetition operators are handled by the Lua pattern matcher. + +Default-mode character classes use .NET character classification. In byte mode, classes and case conversion use ASCII C-locale rules. Thus non-ASCII letters may match `%a` in default mode but not in byte mode. `os.setlocale` does not change these rules. Pattern recursion and capture counts are bounded by the managed matcher. + +## Dumping and loading + +```lua +local bytes = string.dump(function() return 42 end) +local f = assert(load(bytes, 'saved', 'b')) +assert(f() == 42) +``` + +Use byte-mode streams when saving `bytes` from Lua. In C#, use `LuaByteString.ToBytes` to extract its raw bytes. A UTF-16 stream encodes characters as UTF-8 and can corrupt arbitrary binary strings even if opened with `wb`. + +Chunks use the C Lua 5.2 layout, with the architecture/number sizes recorded in their header. Do not assume portability across different Lua versions or incompatible binary layouts. Older Lua-CSharp chunks generated with the previous incorrect header/tag layout must be regenerated from source. `Prototype.StringMode` preserves the representation of constants during dumping. + +Lua 5.3/5.4 additions such as `string.pack`, `string.unpack`, and `string.packsize` are not part of this Lua 5.2 library. diff --git a/docs/stdlib/table.md b/docs/stdlib/table.md new file mode 100644 index 00000000..ae077fec --- /dev/null +++ b/docs/stdlib/table.md @@ -0,0 +1,18 @@ +# Tables + +[Reference index](README.md) · [Official Lua 5.2 table library](https://www.lua.org/manual/5.2/manual.html#6.5) · [Implementation](../../src/Lua/Standard/TableLibrary.cs) + +Install with `state.OpenTableLibrary()`. These functions require a Lua table. They access elements raw, following Lua 5.2 rather than later-version proxy-table behavior. Operations that need a default length honor `__len`; that callback cannot Lua-yield across the library call, although C# code can await. + +| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| --- | --- | --- | +| [`table.concat(t [, sep [, i [, j]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-table.concat) | Joins string/number elements in the inclusive range; defaults are empty separator, `i=1`, and `j=#t`. | Numbers use the platform's `NumberFormat`. Managed string capacity bounds the output size. | +| [`table.insert(t, [pos,] value)`](https://www.lua.org/manual/5.2/manual.html#pdf-table.insert) | Inserts at `pos` and shifts following sequence elements; omitting `pos` appends. Returns no values. | Length and insertion positions are bounded by managed integer indices. A host-frozen table rejects mutation because its published contents are immutable. | +| [`table.pack(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-table.pack) | Returns an argument table with an `n` field holding the argument count, including nil arguments. | | +| [`table.remove(t [, pos])`](https://www.lua.org/manual/5.2/manual.html#pdf-table.remove) | Removes the selected sequence element, shifts later elements, and returns the removed value. Defaults to `#t`. | Host-frozen tables reject mutation. | +| [`table.sort(t [, comp])`](https://www.lua.org/manual/5.2/manual.html#pdf-table.sort) | Sorts the sequence in place using `<` or the supplied comparison function; returns no values. Sorting is not stable. | Comparator/ordering metamethod callbacks cannot Lua-yield; C# comparators can await. Host-frozen tables reject writes. These are managed storage and callback constraints. | +| [`table.unpack(t [, i [, j]])`](https://www.lua.org/manual/5.2/manual.html#pdf-table.unpack) | Returns elements from `i` to `j`, including nils. Defaults to `1` and `#t`; an empty range returns no values. | The result range must fit Lua-CSharp's managed stack capacity; oversized ranges raise `too many results to unpack`. | + +As in Lua 5.2, use a sequence when relying on an implicit length. A table with holes does not have a uniquely useful sequence boundary, and a managed table need not choose the same boundary as a particular C Lua table layout. + +The deprecated `table.maxn` helper and global `unpack` alias are not installed. `table.move` is a later-version addition. See [official library changes](https://www.lua.org/manual/5.2/manual.html#8.2). diff --git a/docs/string-modes.md b/docs/string-modes.md index c22135be..1419b3cb 100644 --- a/docs/string-modes.md +++ b/docs/string-modes.md @@ -45,7 +45,7 @@ In byte mode, `string.char` accepts only 0–255. Non-ASCII letters remain uncha Neither mode normalizes Unicode. For example, C# `"é"` and `"e\u0301"` remain different strings and table keys. -This describes string semantics. Existing Lua-CSharp limitations still apply; for example, `string.dump` is unsupported. C# values still use `string` and require the conversions below, with matching stream configuration. +This describes string semantics. See the [standard library API reference](stdlib/README.md) for per-function differences and limitations. `string.dump` is supported and always returns raw bytes in either mode; see [dumping and loading](stdlib/string.md#dumping-and-loading) before writing its result to a stream. C# values still use `string` and require the conversions below, with matching stream configuration. ## Passing values between C# and Lua From 7faa7f5edcd4e32d9854a80dc9ce55ea400e0cb1 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 02:19:45 +0900 Subject: [PATCH 2/7] docs: correct standard library compatibility notes Audit the per-API reference against Lua 5.2.4 source and the managed implementation. Remove incorrect stripped-name and os.time mutation differences, clarify shared behavior, and document verified conversion, formatting, iteration, and traceback limitations. Validated all 161 official Lua-facing entries and 132 registered functions/file methods, local links and table structure, plus 16 focused comparisons in native Lua 5.2 and both managed string modes. Runtime code is unchanged. --- docs/stdlib/README.md | 8 +++++--- docs/stdlib/basic.md | 12 ++++++------ docs/stdlib/bit32.md | 6 +++--- docs/stdlib/coroutine.md | 6 +++--- docs/stdlib/debug.md | 21 +++++++++++++++------ docs/stdlib/io.md | 8 ++++---- docs/stdlib/math.md | 4 ++-- docs/stdlib/os.md | 18 +++++++++--------- docs/stdlib/package.md | 6 +++--- docs/stdlib/string.md | 12 ++++++++---- docs/stdlib/table.md | 4 ++-- 11 files changed, 60 insertions(+), 45 deletions(-) diff --git a/docs/stdlib/README.md b/docs/stdlib/README.md index a24d98c6..d44aa1e0 100644 --- a/docs/stdlib/README.md +++ b/docs/stdlib/README.md @@ -1,6 +1,6 @@ # Lua standard library reference -This reference describes the Lua-facing APIs installed by Lua-CSharp. Each API links to its entry in the [official Lua 5.2 manual](https://www.lua.org/manual/5.2/manual.html#6). The manual defines the Lua contract; these pages explain the managed implementation, available configuration, and known limitations. +This reference describes the Lua-facing APIs installed by Lua-CSharp. Each API links to its entry in the [official Lua 5.2 manual](https://www.lua.org/manual/5.2/manual.html#6). The manual defines the Lua contract; these pages explain the managed implementation, available configuration, and known limitations. Comparisons with C Lua use the [official Lua 5.2.4 source](https://www.lua.org/source/5.2/), with platform/build differences called out where relevant. ## Open libraries @@ -22,6 +22,8 @@ bytes.OpenStandardLibraries(); Libraries are opt-in. `LuaState.Create()` alone does not install them. To install a subset, call the corresponding extension in the table below. Configure standard I/O before opening its library: changing the provider later does not replace existing handles. Existing module closures also retain their captured package table. +Opening a library again republishes its globals/table; it is not an ensure-loaded operation. For example, reopening math replaces its random generator, and reopening I/O resets the default handles. + | Reference | C# initialization | Official library/global | | --- | --- | --- | | [Basic functions and globals](basic.md) | `OpenBasicLibrary()` | [Basic library](https://www.lua.org/manual/5.2/manual.html#6.1) | @@ -37,7 +39,7 @@ Libraries are opt-in. `LuaState.Create()` alone does not install them. To instal ## Reading the API tables -Signatures use `[optional]` arguments and `...` for multiple values. A blank differences cell means no additional difference is documented for that API; the shared behavior below still applies. These are implementation notes, not a guarantee that every edge case matches every C Lua build. +Signatures use `[optional]` arguments and `...` for multiple values. The last column includes both implementation details and compatibility differences; a managed backend alone does not imply different Lua behavior. A blank notes cell means no additional note is documented for that API; the shared behavior below still applies. These are implementation notes, not a guarantee that every edge case matches every C Lua build. Notes marked **Limitation** describe incomplete compatibility, not a recommended Lua idiom. Notes marked **Host-dependent** depend on a replaceable provider. Explanations identify the implementation mechanism; they do not imply that an existing limitation is intentional. @@ -63,7 +65,7 @@ Host execution cancellation propagates to C# rather than becoming a normal `pcal ### Memory and host objects -The .NET garbage collector owns object lifetimes. Lua GC tuning, weak-table `__mode` semantics, and Lua `__gc` finalizers are not implemented as a separate Lua collector. `collectgarbage` has significant limitations documented in [Basic functions](basic.md). Close files explicitly when their close timing matters. +The .NET garbage collector owns object lifetimes. Lua weak-table `__mode` semantics and Lua `__gc` finalizers are not implemented; .NET collection does not supply these Lua protocols. Lua GC tuning is also unavailable, and `collectgarbage` has significant limitations documented in [Basic functions](basic.md). Close files explicitly when their close timing matters. C# objects can be exposed as userdata, and host functions are wrapped as `LuaFunction` values. Their identity strings and debug metadata describe managed objects rather than native pointers. Host-frozen tables, including generated C# metatables, reject mutation even through raw table operations; this keeps their published interop definitions immutable. diff --git a/docs/stdlib/basic.md b/docs/stdlib/basic.md index ad421d41..58c811e5 100644 --- a/docs/stdlib/basic.md +++ b/docs/stdlib/basic.md @@ -1,25 +1,25 @@ # Basic functions and globals -[Reference index](README.md) · [Official Lua 5.2 basic library](https://www.lua.org/manual/5.2/manual.html#6.1) · [Implementation](../../src/Lua/Standard/BasicLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 basic library](https://www.lua.org/manual/5.2/manual.html#6.1) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/lbaselib.c.html) · [Implementation](../../src/Lua/Standard/BasicLibrary.cs) Install with `state.OpenBasicLibrary()`. Functions are globals. The [shared string, numeric-text, cancellation, and host-object behavior](README.md#shared-behavior) applies throughout. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`assert(v [, message])`](https://www.lua.org/manual/5.2/manual.html#pdf-assert) | Returns all arguments when `v` is truthy; otherwise raises an assertion error. The default message is `assertion failed!`. | The host receives a `LuaAssertionException`. Lua 5.2's message is string-like: a number is converted to text; an arbitrary table is not an assertion error object. | | [`collectgarbage([opt [, arg]])`](https://www.lua.org/manual/5.2/manual.html#pdf-collectgarbage) | Recognizes `collect`, `stop`, `restart`, `count`, `step`, `setpause`, `setstepmul`, `setmajorinc`, `isrunning`, `incremental`, and `generational`. | **Limitation:** every recognized option calls `GC.Collect()` and returns **no values**. `arg` is ignored. Thus `count` does not report memory, `stop` does not stop GC, and tuning/mode options do not tune it. The implementation delegates to .NET GC without implementing Lua's collector control protocol. | | [`dofile([filename])`](https://www.lua.org/manual/5.2/manual.html#pdf-dofile) | Loads and executes a file; returns its results and raises load/runtime failures. | **Limitation:** a filename is required; omitted/nil input does not load standard input. Loading goes through `LuaPlatform.FileSystem`. Paths are decoded to .NET text in byte mode. | -| [`error(message [, level])`](https://www.lua.org/manual/5.2/manual.html#pdf-error) | Raises the supplied error object. `level` selects the source prefix for string errors; `0` suppresses that prefix. | Host-facing errors use `LuaRuntimeException`; stack descriptions include managed frames. | +| [`error(message [, level])`](https://www.lua.org/manual/5.2/manual.html#pdf-error) | Raises the supplied error object. `level` selects the source prefix for string errors; `0` suppresses that prefix. | Host-facing errors use `LuaRuntimeException`; stack descriptions include managed frames. **Limitation:** numeric messages retain their number type even at positive levels. C Lua converts them to strings when adding source information; the managed exception preserves the original value. At level 0 both preserve a numeric object. | | [`_G`](https://www.lua.org/manual/5.2/manual.html#pdf-_G) | The initial global environment table. | Installed from the state's managed environment. Assigning `_G` does not rewrite a function's `_ENV`. | | [`getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-getmetatable) | Returns the metatable, its protected `__metatable` value, or nil. | Also works with managed userdata. | | [`ipairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-ipairs) | Uses `__ipairs` when present; otherwise returns an iterator, the table, and `0`, stopping at the first missing integer element. | **Limitation:** directly calling the default iterator with a fractional control value increments that double instead of first converting it to a C integer. At exhaustion it returns two nils rather than one. The ordinary generic-for sequence use is unaffected. `__ipairs` cannot yield across the library callback boundary. | | [`load(ld [, source [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-load) | Compiles source/bytecode from a string or a reader function. Returns a function, or `nil, error`; an empty reader result ends input. `mode` defaults to `bt`. Explicit `env`, including nil, initializes the first upvalue. | **Limitation:** reader pieces are collected before parsing, so readers can be called past the point where C Lua would detect a syntax error or finish a binary chunk. This follows the current whole-input compiler interface. Reader callbacks cannot Lua-yield, but C# readers can await. Raw binary strings work in either mode; see [dumping](string.md#dumping-and-loading). | | [`loadfile([filename [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-loadfile) | Compiles a file without running it; returns a function or `nil, error`. Recognizes an initial UTF-8 BOM and first-line `#` comment. | **Limitation:** filename is required; there is no standard-input overload in the Lua library. Uses the configured filesystem, with byte-mode paths decoded to .NET text. | -| [`next(t [, index])`](https://www.lua.org/manual/5.2/manual.html#pdf-next) | Returns the next key/value, or nil after the last key. Omitted/nil `index` starts traversal. | Order follows the managed table storage and is unspecified. Do not depend on matching C Lua's hash iteration order. | +| [`next(t [, index])`](https://www.lua.org/manual/5.2/manual.html#pdf-next) | Returns the next key/value, or nil after the last key. Omitted/nil `index` starts traversal. | Order follows the managed table storage and is unspecified. **Limitation:** an unknown hash key such as `next({}, 'missing')` returns nil instead of C Lua's invalid-key error: the managed lookup treats that missing iteration position as the end. | | [`pairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-pairs) | Uses `__pairs`, or returns the default iterator, table, and nil. | `__pairs` cannot Lua-yield across the library callback boundary. | | [`pcall(f [, ...])`](https://www.lua.org/manual/5.2/manual.html#pdf-pcall) | Returns `true, ...results`, or `false, errorObject`. Callable values with `__call` are supported. | Host execution cancellation escapes to C# rather than being consumed as a script error. | -| [`print(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-print) | Calls the global `tostring` for each argument and writes tab-separated output plus a newline. | **Host-dependent:** writes to `LuaPlatform.StandardIO.Output`, not the file selected by `io.output`. The output provider may be a console, redirected stream, or Unity logger. Numeric text uses `NumberFormat`; the `tostring` callbacks cannot Lua-yield. | -| [`rawequal(a, b)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawequal) | Compares values without `__eq`. | Managed object identity applies to reference-backed values. | +| [`print(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-print) | Calls the global `tostring` for each argument and writes tab-separated output plus a newline. | **Host-dependent:** writes to `LuaPlatform.StandardIO.Output`, which may be a console, redirected stream, or Unity logger. As in C Lua, selecting `io.output` does not redirect `print`. Numeric text uses `NumberFormat`; the `tostring` callbacks cannot Lua-yield. | +| [`rawequal(a, b)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawequal) | Compares values without `__eq`; strings compare by contents. | Tables, functions, threads, and userdata use managed object identity, corresponding to Lua's identity comparison for these types. | | [`rawget(t, key)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawget) | Reads a table slot without `__index`. | | | [`rawlen(v)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawlen) | Returns the raw table length or string length without `__len`. | String length counts UTF-16 code units in default mode and bytes in byte mode. | | [`rawset(t, key, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-rawset) | Writes without `__newindex` and returns `t`. | Host-frozen tables still reject writes; raw access bypasses metamethods, not the managed table's read-only state. | diff --git a/docs/stdlib/bit32.md b/docs/stdlib/bit32.md index 1e306454..23a57b2b 100644 --- a/docs/stdlib/bit32.md +++ b/docs/stdlib/bit32.md @@ -1,12 +1,12 @@ # Bitwise operations -[Reference index](README.md) · [Official Lua 5.2 bit32 library](https://www.lua.org/manual/5.2/manual.html#6.7) · [Implementation](../../src/Lua/Standard/BitwiseLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 bit32 library](https://www.lua.org/manual/5.2/manual.html#6.7) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/lbitlib.c.html) · [Implementation](../../src/Lua/Standard/BitwiseLibrary.cs) Install with `state.OpenBitwiseLibrary()`. Results are doubles representing unsigned 32-bit words, not a separate integer type. -Word arguments are rounded with .NET `Math.Round` (nearest, ties to even) and reduced modulo 2^32. Lua 5.2 permits implementation-dependent conversion of fractional words; this is Lua-CSharp's choice, matching the usual IEEE-754 Lua 5.2 conversion. Shift amounts, field positions, and widths are converted separately by truncating to managed integers. Use integral arguments when matching results across implementations. +Word arguments are rounded with .NET `Math.Round` (nearest, ties to even) and reduced modulo 2^32. Lua 5.2 permits implementation-dependent conversion of fractional words; these are Lua-CSharp's rules, and a C build's conversion may differ. Shift amounts, field positions, and widths are converted separately by truncating to managed integers. Use integral arguments when matching results across implementations. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`bit32.arshift(x, disp)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.arshift) | Arithmetic right shift with sign extension; negative displacement shifts left. | Uses signed 32-bit shifting internally and returns the unsigned word. | | [`bit32.band(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-bit32.band) | Bitwise AND; no arguments returns all 32 bits set. | | diff --git a/docs/stdlib/coroutine.md b/docs/stdlib/coroutine.md index 73c17a66..1b6df75c 100644 --- a/docs/stdlib/coroutine.md +++ b/docs/stdlib/coroutine.md @@ -1,12 +1,12 @@ # Coroutines -[Reference index](README.md) · [Official Lua 5.2 coroutine library](https://www.lua.org/manual/5.2/manual.html#6.2) · [Implementation](../../src/Lua/Standard/CoroutineLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 coroutine library](https://www.lua.org/manual/5.2/manual.html#6.2) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/lcorolib.c.html) · [Implementation](../../src/Lua/Standard/CoroutineLibrary.cs) Install with `state.OpenCoroutineLibrary()`. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | -| [`coroutine.create(f)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.create) | Creates a suspended coroutine whose body is `f`; returns a `thread`. | Also accepts a C# `LuaFunction` body, since host functions participate in the same managed call machinery. | +| [`coroutine.create(f)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.create) | Creates a suspended coroutine whose body is `f`; returns a `thread`. | A C# `LuaFunction` can be the body, analogous to using a C function in C Lua. Host functions participate in the managed call machinery. | | [`coroutine.resume(co [, ...])`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.resume) | Starts/resumes `co`; returns `true` followed by yielded or final results, or `false, error` for a script failure. Resume arguments become body arguments or yield results. | Can await asynchronous C# work. Host cancellation follows the managed execution cancellation protocol rather than becoming an ordinary script failure. | | [`coroutine.running()`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.running) | Returns the current thread and a boolean identifying the main thread. | Threads are represented by `LuaState` objects. They are not dedicated OS threads. | | [`coroutine.status(co)`](https://www.lua.org/manual/5.2/manual.html#pdf-coroutine.status) | Reports `running`, `suspended`, `normal`, or `dead`. | Awaiting C# work does not itself create a Lua yield/resume exchange. | diff --git a/docs/stdlib/debug.md b/docs/stdlib/debug.md index 3f63c86a..e6d3565c 100644 --- a/docs/stdlib/debug.md +++ b/docs/stdlib/debug.md @@ -1,25 +1,25 @@ # Debugging -[Reference index](README.md) · [Official Lua 5.2 debug library](https://www.lua.org/manual/5.2/manual.html#6.10) · [Implementation](../../src/Lua/Standard/DebugLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 debug library](https://www.lua.org/manual/5.2/manual.html#6.10) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/ldblib.c.html) · [Implementation](../../src/Lua/Standard/DebugLibrary.cs) Install with `state.OpenDebugLibrary()`. Thread-aware functions accept an optional first `thread`; otherwise they inspect the current state. Metadata describes Lua bytecode and managed call frames, not a native Lua C stack. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`debug.debug()`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.debug) | Officially enters an interactive debug command loop. | **Unavailable:** not registered. Interactive console/debugger integration must be provided by the host. | -| [`debug.gethook([thread])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.gethook) | Returns the installed hook, mask, and count. With no hook: nil, empty mask, and zero count. | A host-installed hook is a managed `LuaFunction`, not a native C hook pointer. Its execution policy can differ from a Lua-installed hook; see below. | +| [`debug.gethook([thread])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.gethook) | Returns the installed hook, mask, and count. After removing the hook: nil, empty mask, and zero count. | A host-installed hook is returned as its managed `LuaFunction`; C Lua reports a hook installed through its C API as the string `"external hook"`. The host hook's execution policy can differ from a Lua-installed hook; see below. | | [`debug.getinfo([thread,] f [, what])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getinfo) | Returns function/frame metadata selected by `n`, `S`, `l`, `u`, `t`, `f`, and `L`, or nil for a missing stack level. | C# functions report `what="C#"` and `source="=[C#]"` instead of C-function labels. `u` exposes managed closure capture counts. **Limitation:** numeric strings are not accepted as stack levels, because the function/level dispatch tests the Lua value tag before numeric coercion. | | [`debug.getlocal([thread,] f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getlocal) | Inspects a frame local/temporary/vararg, or a Lua function's parameter name; returns nil when no such local exists. | Managed frames expose their Lua-facing argument/temporary slots, not arbitrary C# local variables. The implementation operates on Lua-CSharp stack metadata. | | [`debug.getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getmetatable) | Returns the actual metatable, bypassing `__metatable` protection. | May return a host-frozen generated metatable. Reading the actual table does not make its contents writable. | | [`debug.getregistry()`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getregistry) | Returns the state's registry table. | Registry contents are managed runtime state; C API integer-slot conventions and native pointers are not a portable contract for Lua-CSharp. | -| [`debug.getupvalue(f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getupvalue) | Returns an upvalue name/value, or no values for an invalid slot. | Supports Lua closures and `CSharpClosure.UpValues`. Managed capture names are empty strings. **Limitation:** stripped Lua upvalue names also remain empty instead of C Lua's `(no name)` placeholder, because missing serialized names are stored as empty strings. | +| [`debug.getupvalue(f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getupvalue) | Returns an upvalue name/value, or no values for an invalid slot. | Supports Lua closures and `CSharpClosure.UpValues`. Managed capture names and stripped Lua upvalue names are empty strings, as with C-function captures and stripped Lua names in C Lua 5.2.4. | | [`debug.getuservalue(u)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getuservalue) | Reads a userdata's first user-value slot, or nil when absent. | Uses `ILuaUserData.UserValues`. Host userdata can expose no slots; values are `LuaValue` entries rather than Lua 5.2's table/nil-only uservalue model. | | [`debug.sethook([thread,] hook, mask [, count])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.sethook) | Installs/removes a hook; mask letters are `c`, `r`, and `l`, with an optional instruction count. | Lua-installed hooks cannot Lua-yield. They can call C# code that awaits. Counting is based on VM instructions, not time spent inside a C# function. See [hook details](#hook-details). | | [`debug.setlocal([thread,] level, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setlocal) | Writes a frame local and returns its name, or nil when unavailable. | Writes Lua-facing managed stack slots; it cannot set arbitrary CLR locals. | | [`debug.setmetatable(value, mt)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setmetatable) | Replaces/removes the actual metatable, bypassing `__metatable` protection; returns the value. | Host-frozen tables still reject changing their metatable because `LuaTable.Freeze()` protects managed storage. **Limitation:** light-userdata object tokens have no case in the managed metatable dispatcher, so setting their metatable has no effect. | -| [`debug.setupvalue(f, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setupvalue) | Writes an upvalue and returns its name, or no values for an invalid slot. | Can update managed closure capture slots; their names are empty. Missing stripped Lua names have the same placeholder limitation as `getupvalue`. | +| [`debug.setupvalue(f, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setupvalue) | Writes an upvalue and returns its name, or no values for an invalid slot. | Can update managed closure capture slots; their names are empty. Stripped Lua names are also empty, consistently with `getupvalue` and C Lua 5.2.4. | | [`debug.setuservalue(u, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setuservalue) | Writes the first user-value slot and returns the userdata. | **Difference/limitation:** any Lua value is accepted, rather than Lua 5.2's table/nil restriction. Userdata exposing no `UserValues` storage returns nil. This reflects the host-defined `Span` storage API. | -| [`debug.traceback([thread,] [message [, level]])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.traceback) | Builds a traceback, passing non-string/non-number error objects through unchanged. | Formats managed/C# frames and Lua source metadata rather than reproducing C Lua's exact trace text. **Limitations:** explicit negative levels return nil; supplying the current thread explicitly defaults to level 0 instead of C Lua's current-thread level 1. Numeric messages use the managed value's `ToString`, independently of `NumberFormat`. These follow the current formatter/default-level implementation. | +| [`debug.traceback([thread,] [message [, level]])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.traceback) | Builds a traceback; non-string/non-number messages pass through unchanged after level validation. | Formats managed/C# frames and Lua source metadata rather than reproducing C Lua's exact trace text. See [traceback details](#traceback-details) for argument and formatting limitations. | | [`debug.upvalueid(f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.upvalueid) | Returns an identity token for an upvalue slot; joined Lua slots share an identity. | Uses weakly associated managed identity objects instead of a native address. Retaining the token does not retain the captured payload; this fits managed GC without exposing movable CLR storage addresses. | | [`debug.upvaluejoin(f1, n1, f2, n2)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.upvaluejoin) | Makes one Lua closure's slot share another Lua closure's upvalue. | Restricted to Lua closures, as in Lua 5.2. Managed capture arrays can be inspected/updated but are not joined by this API. | @@ -30,3 +30,12 @@ Hook callbacks receive an event string and a line number or nil. Events include Hooks installed from C# through `LuaState.SetHook` retain the host hook policy, including supported async/yield behavior; they are distinguished internally from `debug.sethook` callbacks. This lets host instrumentation integrate with async execution while keeping Lua-installed callbacks compatible with non-yieldable Lua 5.2 hooks. See the [C# debug-hook guide](../../README.md#debug-hooks). Stripped bytecode has no line/local names to recover. Source/line fields and local lookup reflect the metadata actually present; dumping and reloading a stripped chunk cannot reconstruct it. **Limitation:** `getinfo` can report an unknown Lua line as `0` instead of C Lua's `-1`, because the managed line lookup uses zero when line info is absent. + +## Traceback details + +The current formatter and argument handling have these limitations: + +- Explicit negative levels return nil rather than a traceback string. +- Supplying the current thread explicitly defaults to level 0 instead of C Lua's current-thread level 1. +- Numeric messages use the managed value's `ToString`, independently of `NumberFormat`. +- The level is validated before passing through a non-string/non-number message. For example, `debug.traceback({}, 'bad')` errors; C Lua returns the table without inspecting the level. diff --git a/docs/stdlib/io.md b/docs/stdlib/io.md index eca25969..6f9de3d8 100644 --- a/docs/stdlib/io.md +++ b/docs/stdlib/io.md @@ -1,12 +1,12 @@ # Files and standard I/O -[Reference index](README.md) · [Official Lua 5.2 I/O library](https://www.lua.org/manual/5.2/manual.html#6.8) · [Library implementation](../../src/Lua/Standard/IOLibrary.cs) · [File methods](../../src/Lua/Standard/FileHandle.cs) +[Reference index](README.md) · [Official Lua 5.2 I/O library](https://www.lua.org/manual/5.2/manual.html#6.8) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/liolib.c.html) · [Library implementation](../../src/Lua/Standard/IOLibrary.cs) · [File methods](../../src/Lua/Standard/FileHandle.cs) Install with `state.OpenIOLibrary()`. Operations use `LuaPlatform.FileSystem` and the standard-stream handles captured from `LuaPlatform.StandardIO` when the library opens. A custom provider can use memory, assets, or another storage system. ## Library APIs -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`io.close([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.close) | Closes the supplied file, or the default output when no argument is supplied. Explicit nil is an error. | Default console standard handles reject closing. File failures use managed messages/error codes; see below. | | [`io.flush()`](https://www.lua.org/manual/5.2/manual.html#pdf-io.flush) | Flushes the current default output. | Calls the configured stream's flush operation; it is not necessarily an OS file stream. | @@ -25,13 +25,13 @@ Install with `state.OpenIOLibrary()`. Operations use `LuaPlatform.FileSystem` an ## File methods -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`file:close()`](https://www.lua.org/manual/5.2/manual.html#pdf-file:close) | Closes the underlying stream; reports success or handled I/O failure. | Calls `ILuaStream.CloseAsync`; close behavior belongs to the provider. The default stream flushes and disposes the underlying .NET stream. Lua GC finalizers are not used to implement deterministic close. | | [`file:flush()`](https://www.lua.org/manual/5.2/manual.html#pdf-file:flush) | Flushes pending writes. | Calls the provider's flush operation. | | [`file:lines(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:lines) | Returns an iterator over the file using the given formats; does not automatically close the handle. | Closed handles fail at iterator creation. **Limitation:** the EOF result count follows the same nil-versus-no-results difference as `io.lines`. | | [`file:read(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:read) | Returns one value per successful format, stopping at the first failed conversion/EOF with nil. No formats means `*l`. | Default streams decode UTF-8 to UTF-16, while byte-mode streams preserve raw bytes. Counts use the selected representation. Numeric scanning remains byte-based, even in UTF-16 mode. | -| [`file:seek([whence [, offset]])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:seek) | Seeks using `set`, `cur`, or `end`; defaults are `cur, 0`. Returns the resulting position or handled I/O failure. | Offsets/positions are **bytes** in built-in streams, including UTF-16 mode. Buffered unread bytes are accounted for. If a counted read returned only the high surrogate of a supplementary character, its bytes have already been consumed; a position query retains the pending low surrogate. Underlying seek support is provider-dependent. | +| [`file:seek([whence [, offset]])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:seek) | Seeks using `set`, `cur`, or `end`; defaults are `cur, 0`. Requires an integral offset, as in C Lua 5.2.4. Returns the resulting position or handled I/O failure. | Offsets/positions are **bytes** in built-in streams, including UTF-16 mode. Buffered unread bytes are accounted for. If a counted read returned only the high surrogate of a supplementary character, its bytes have already been consumed; a position query retains the pending low surrogate. **Limitation:** the offset uses a strict managed `long` read, so numeric strings such as `'1'` are rejected instead of coerced as in C Lua. Underlying seek support is provider-dependent. | | [`file:setvbuf(mode [, size])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:setvbuf) | Selects `no`, `line`, or `full` buffering policy; returns true when accepted. | **Limitation:** built-in `LuaStream` changes flush policy, not the underlying .NET buffer allocation. `no` flushes each write; `line` flushes writes containing LF; `full` uses a per-write size threshold rather than a cumulative C stdio buffer. `size` does not resize the stream's buffer. Custom providers may implement their own policy. | | [`file:write(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:write) | Writes strings/numbers and returns the file on success. | Default streams encode UTF-16 text as UTF-8; byte-mode streams write raw byte values. Numbers use `NumberFormat`. | diff --git a/docs/stdlib/math.md b/docs/stdlib/math.md index f44160a1..0fa68b46 100644 --- a/docs/stdlib/math.md +++ b/docs/stdlib/math.md @@ -1,12 +1,12 @@ # Mathematics -[Reference index](README.md) · [Official Lua 5.2 math library](https://www.lua.org/manual/5.2/manual.html#6.6) · [Implementation](../../src/Lua/Standard/MathematicsLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 math library](https://www.lua.org/manual/5.2/manual.html#6.6) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/lmathlib.c.html) · [Implementation](../../src/Lua/Standard/MathematicsLibrary.cs) Install with `state.OpenMathLibrary()`. Numbers are .NET doubles. Most operations call `System.Math`; decomposition/scaling use managed helpers. Transcendental rounding and exceptional-value details can vary with the .NET runtime/platform, just as C math libraries vary between Lua builds. No C math library or mutable C locale is used. Angles are radians unless an API explicitly converts degrees. The [numeric text policy](README.md#numbers-and-text) affects printing results, not the calculations. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`math.abs(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.abs) | Absolute value. | `Math.Abs`. | | [`math.acos(x)`](https://www.lua.org/manual/5.2/manual.html#pdf-math.acos) | Inverse cosine. | `Math.Acos`; managed floating-point behavior applies. | diff --git a/docs/stdlib/os.md b/docs/stdlib/os.md index 7cf26ebc..c1fa6835 100644 --- a/docs/stdlib/os.md +++ b/docs/stdlib/os.md @@ -1,35 +1,35 @@ # Operating system -[Reference index](README.md) · [Official Lua 5.2 OS library](https://www.lua.org/manual/5.2/manual.html#6.9) · [Implementation](../../src/Lua/Standard/OperatingSystemLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 OS library](https://www.lua.org/manual/5.2/manual.html#6.9) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/loslib.c.html) · [Implementation](../../src/Lua/Standard/OperatingSystemLibrary.cs) Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process exit, and filesystem operations are routed through `LuaPlatform` providers where indicated. Default providers use .NET APIs; Unity/custom providers can behave differently. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`os.clock()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.clock) | Returns elapsed CPU time in seconds. | **Host-dependent:** calls `ILuaOsEnvironment.GetTotalProcessorTime`. The default reads the current .NET process's total processor time, including host work outside Lua. | | [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. | Uses `DateTime`, `TimeProvider`, and invariant .NET formatting rather than C `strftime`. **Limitation:** several conversions are incomplete or differ; see below. | -| [`os.difftime(t2, t1)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns `t2 - t1` in seconds. | Direct double subtraction, matching the Unix-seconds representation used here. | +| [`os.difftime(t2, t1)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns `t2 - t1` in seconds. | Direct double subtraction preserves fractions instead of converting to C `time_t` first. **Limitation:** both arguments are required; C Lua 5.2.4 also accepts an omitted/nil second argument as zero, although the manual lists both arguments. | | [`os.execute([command])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.execute) | Officially runs a shell command or queries shell availability. | **Unsupported operation:** no command/nil returns false; supplying a command raises a not-supported error. The library has no shell-execution implementation. | -| [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitation:** the `close` argument is ignored, so this API does not explicitly dispose the Lua state before exiting. | +| [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitations:** `close` is ignored, so the Lua state is not explicitly disposed before exiting. The numeric branch requires an integral 32-bit Lua number through a strict managed `int` read; fractional numbers and numeric strings are rejected instead of converted by C Lua's integer argument reader. | | [`os.getenv(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.getenv) | Returns an environment value or nil. | **Host-dependent:** default provider reads process environment variables; custom/Unity providers may expose a managed dictionary instead. Names/results are converted between byte strings and .NET text when needed. | | [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a path or reports failure. | **Limitation:** default `FileSystem.Remove` removes files only, even on POSIX hosts where C Lua can remove empty directories. It uses `File.Exists`/`File.Delete` and has no directory-removal path. Handled errors use .NET HResult codes. | | [`os.rename(oldname, newname)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.rename) | Moves/renames a file or directory; reports success or handled failure. | **Host-dependent:** default provider uses `File.Move`/`File.Replace` and `Directory.Move`. Directory replacement restrictions follow .NET/host filesystem behavior rather than C `rename`; .NET Standard 2.1 uses `File.Replace` for an existing file destination. | -| [`os.setlocale(locale [, category])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Officially selects or queries a C locale. | **Limitation:** only the literal `C` is accepted; other strings return nil, nil is not a locale-query operation, and `category` is ignored. No process C locale is changed; the implementation uses invariant managed formatting and mode-specific string rules. | -| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. | **Limitation:** constructs a .NET `DateTime`, so out-of-range fields are rejected rather than normalized by C `mktime`. `isdst` is ignored and the input table is not rewritten with normalized fields. Default current time comes from `TimeProvider`. | +| [`os.setlocale(locale [, category])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Officially selects or queries a C locale. | **Limitation:** only the literal `C` is accepted; other strings return nil, nil is not a locale-query operation, and `category` is ignored. No C or .NET locale is changed. Date formatting stays invariant; string casing/classification follows the [string library's rules](string.md). | +| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. `isdst` is ignored. Current time comes from `TimeProvider`; table conversion uses the system local timezone. | | [`os.tmpname()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.tmpname) | Returns a temporary pathname. | **Host-dependent:** default `Path.GetTempFileName` creates an empty file while choosing the name. The caller owns cleanup; unlike `io.tmpfile`, this API does not attach delete-on-close behavior. The name uses the state's string representation. | ## Date conversion details The date formatter is a managed mapping, not a complete C `strftime` implementation. The following conversions are recognized: -`%a %A %b %B %c %C %d %D %e %F %g %G %h %H %I %j %m %M %n %p %r %R %S %t %T %u %w %x %X %y %Y %z %Z %%` +`%a %A %b %B %c %C %d %D %e %F %g %G %h %H %I %j %m %M %n %p %r %R %S %t %T %u %U %V %w %W %x %X %y %Y %z %Z %%` Known formatting differences and incomplete conversions include: - `%U`, `%V`, and `%W` currently emit the literal `??`; week-number calculation is not implemented. - `%g`/`%G` use the calendar year, not the ISO week-based year defined for those conversions by POSIX `strftime`. Some C Lua builds reject these conversions entirely. -- `%z` and `%Z` both use .NET's `zzz` offset format, including a colon, rather than separate C numeric-offset/timezone-name conversions. -- `%c`, `%x`, `%X`, month/day names, and related patterns use invariant .NET formatting; changing `os.setlocale` has no effect. +- `%z` and `%Z` both use .NET's `zzz` offset format, including a colon, rather than separate C numeric-offset/timezone-name conversions. With omitted time, the formatter receives a `DateTime` without timezone-kind information, so even a leading `!` can produce the system local offset in these two conversions. +- `%c`, `%x`, `%X`, month/day names, and related patterns use invariant .NET formatting; changing `os.setlocale` has no effect. In particular, `%c` maps to .NET's long-date/short-time pattern and omits seconds; `%x` uses its short-date pattern; `%r` uses an unpadded 12-hour clock hour. - `%E`/`%O` modifiers and unknown conversions raise an error. Timestamp conversion is bounded by .NET date/time ranges. For byte-string states, format literals are represented as byte-string units; the formatter's generated names come from invariant .NET date strings. Use the official link for the complete Lua/C contract and this list for the implemented behavior. diff --git a/docs/stdlib/package.md b/docs/stdlib/package.md index b62075f1..c8996bb6 100644 --- a/docs/stdlib/package.md +++ b/docs/stdlib/package.md @@ -1,13 +1,13 @@ # Packages and modules -[Reference index](README.md) · [Official Lua 5.2 package library](https://www.lua.org/manual/5.2/manual.html#6.3) · [Implementation](../../src/Lua/Standard/ModuleLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 package library](https://www.lua.org/manual/5.2/manual.html#6.3) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/loadlib.c.html) · [Implementation](../../src/Lua/Standard/ModuleLibrary.cs) Install with `state.OpenModuleLibrary()`. This installs global `require` and the `package` table. Each opened standard library is registered in the loaded-module table. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`require(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-require) | Returns a truthy cached module or searches for a loader. Calls the loader with `name` and searcher data; caches a non-nil result, or uses the loaded entry/default `true`. A false cache entry allows a later reload. | **Host-dependent:** a configured `ILuaModuleLoader` is consulted before package searchers when it reports the module exists. This lets C# supply resources/assets without native filesystem loading. The package object is captured when the library is opened, so replacing global `package` does not retarget an existing `require`. Loader/searcher callbacks cannot Lua-yield, but host loaders can await. | -| [`package.config`](https://www.lua.org/manual/5.2/manual.html#pdf-package.config) | A newline-delimited string describing directory, path-list, substitution, executable-directory, and ignore markers. | The directory character is the host .NET separator; the remaining markers are `;`, `?`, `!`, and `-`. This is initialized from the host runtime, not a C Lua build configuration. | +| [`package.config`](https://www.lua.org/manual/5.2/manual.html#pdf-package.config) | A newline-delimited string describing directory, path-list, substitution, executable-directory, and ignore markers. | The directory character is the host .NET separator; the remaining markers are `;`, `?`, `!`, and `-`. **Limitation:** the initializer omits C Lua's final newline, so a parser requiring a newline after every marker can miss the last one. | | [`package.cpath`](https://www.lua.org/manual/5.2/manual.html#pdf-package.cpath) | Officially configures native C module search paths. | **Unavailable:** no field or native C-module searcher is installed. Lua-CSharp uses managed bindings and host module loaders rather than Lua's C ABI. | | [`package.loaded`](https://www.lua.org/manual/5.2/manual.html#pdf-package.loaded) | The table used to cache module values. | Backed by the state's managed loaded-module table. As in Lua 5.2, replacing the `package.loaded` field does not replace the cache used by `require`; mutate its entries instead. | | [`package.loadlib(libname, funcname)`](https://www.lua.org/manual/5.2/manual.html#pdf-package.loadlib) | Officially loads a native library and its Lua C entry point. | **Unavailable:** not registered. A native Lua C function cannot run directly against Lua-CSharp's managed state/stack representation. Expose C# functions or an `ILuaModuleLoader` instead. | diff --git a/docs/stdlib/string.md b/docs/stdlib/string.md index c7e567cf..0f5580a0 100644 --- a/docs/stdlib/string.md +++ b/docs/stdlib/string.md @@ -1,18 +1,18 @@ # Strings -[Reference index](README.md) · [Official Lua 5.2 string library](https://www.lua.org/manual/5.2/manual.html#6.4) · [Implementation](../../src/Lua/Standard/StringLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 string library](https://www.lua.org/manual/5.2/manual.html#6.4) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/lstrlib.c.html) · [Implementation](../../src/Lua/Standard/StringLibrary.cs) Install with `state.OpenStringLibrary()`. String method calls such as `s:sub(2)` use this library through the string metatable. The central difference is representation: operations use UTF-16 code units by default and bytes in `LuaStringMode.Bytes`. This makes default-mode operations convenient for .NET strings; byte mode preserves Lua's binary-string model. See [String modes](../string-modes.md). Indices are 1-based; negative substring indices count from the end. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`string.byte(s [, i [, j]])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.byte) | Returns the numeric units in the inclusive range, defaulting to the first unit. | Default mode returns UTF-16 code-unit values, including surrogate values. Byte mode returns 0–255. The managed string representation determines the units. | | [`string.char(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.char) | Constructs a string from numeric character values. | Byte mode requires 0–255, as Lua byte strings do. Default mode casts the integer values to UTF-16 `char`, rather than enforcing a byte range; use ordinary text or byte mode according to the intended data. | | [`string.dump(f)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.dump) | Serializes a Lua function's prototype as a raw binary string. Captured values are not serialized. C# functions cannot be dumped. | Available in both string modes; the result always holds raw byte values. See [dumping and loading](#dumping-and-loading). | | [`string.find(s, pattern [, init [, plain]])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.find) | Returns match start/end and captures, or nil. `plain` selects literal substring search. | Positions use the state's string units. Pattern character classes use the rules below; plain search uses ordinal string matching. | -| [`string.format(format, ...)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.format) | Formats values using Lua's printf-style conversions, including `%s`, `%q`, integer, floating-point, and hexadecimal floating-point conversions. | Width/precision count managed string units. `%c` constructs a UTF-16 unit in default mode and masks to one byte in byte mode. Numeric text and special values follow `NumberFormat` where applicable. Integer formatting uses 64-bit signed/unsigned ranges; these can differ from a C Lua build's configured integer formatting width. | +| [`string.format(format, ...)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.format) | Formats values using Lua's printf-style conversions, including `%s`, `%q`, integer, floating-point, and hexadecimal floating-point conversions. | Width/precision count managed string units. `%c` constructs a UTF-16 unit in default mode and masks to one byte in byte mode. Numeric text and special values follow `NumberFormat` where applicable. Integer formatting uses 64-bit signed/unsigned ranges; these can differ from a C Lua build's configured integer formatting width. See [format details](#format-details). | | [`string.gmatch(s, pattern)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.gmatch) | Returns an iterator over captures, or whole matches when there are no captures. | Positions and classes follow the selected string mode. | | [`string.gsub(s, pattern, replacement [, n])`](https://www.lua.org/manual/5.2/manual.html#pdf-string.gsub) | Returns the replaced string and match count. Replacement may be text, a number, a table, or a function. False/nil replacement results retain the matched text. | Numeric replacements use `NumberFormat`. Table `__index` and replacement callbacks can await C# work but cannot Lua-yield through this library call. | | [`string.len(s)`](https://www.lua.org/manual/5.2/manual.html#pdf-string.len) | Returns the string length, including embedded zero units. | Counts UTF-16 units by default and bytes in byte mode; neither counts Unicode grapheme clusters. | @@ -27,7 +27,11 @@ The central difference is representation: operations use UTF-16 code units by de [Official pattern syntax](https://www.lua.org/manual/5.2/manual.html#6.4.1) applies: Lua patterns are not .NET regular expressions. Captures, position captures `()`, balanced matches `%b`, frontier patterns `%f`, anchors, and Lua repetition operators are handled by the Lua pattern matcher. -Default-mode character classes use .NET character classification. In byte mode, classes and case conversion use ASCII C-locale rules. Thus non-ASCII letters may match `%a` in default mode but not in byte mode. `os.setlocale` does not change these rules. Pattern recursion and capture counts are bounded by the managed matcher. +Default-mode letter, digit, case, control, whitespace, and graphical classes use .NET character classification. `%p` uses ASCII punctuation ranges, `%x` uses ASCII hexadecimal digits, and `%z` matches NUL. In byte mode, all classes use ASCII C-locale rules. Thus non-ASCII letters may match `%a` in default mode but not in byte mode. `os.setlocale` does not change these rules. Pattern recursion and capture counts are bounded by the managed matcher. + +## Format details + +Managed `%s` formatting preserves embedded NUL units. For example, `string.format('%5s', string.char(97, 0, 98))` produces two spaces followed by `a`, NUL, and `b`. C Lua 5.2.4 passes this short string to C `sprintf`, which stops at the NUL and produces four spaces followed by `a`. C Lua has a separate path for long strings without precision, so its `%s` NUL behavior is not uniform across inputs. ## Dumping and loading diff --git a/docs/stdlib/table.md b/docs/stdlib/table.md index ae077fec..b7396f2c 100644 --- a/docs/stdlib/table.md +++ b/docs/stdlib/table.md @@ -1,10 +1,10 @@ # Tables -[Reference index](README.md) · [Official Lua 5.2 table library](https://www.lua.org/manual/5.2/manual.html#6.5) · [Implementation](../../src/Lua/Standard/TableLibrary.cs) +[Reference index](README.md) · [Official Lua 5.2 table library](https://www.lua.org/manual/5.2/manual.html#6.5) · [C Lua 5.2.4 source](https://www.lua.org/source/5.2/ltablib.c.html) · [Implementation](../../src/Lua/Standard/TableLibrary.cs) Install with `state.OpenTableLibrary()`. These functions require a Lua table. They access elements raw, following Lua 5.2 rather than later-version proxy-table behavior. Operations that need a default length honor `__len`; that callback cannot Lua-yield across the library call, although C# code can await. -| API / official reference | Inputs and result | Lua-CSharp differences and reasons | +| API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`table.concat(t [, sep [, i [, j]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-table.concat) | Joins string/number elements in the inclusive range; defaults are empty separator, `i=1`, and `j=#t`. | Numbers use the platform's `NumberFormat`. Managed string capacity bounds the output size. | | [`table.insert(t, [pos,] value)`](https://www.lua.org/manual/5.2/manual.html#pdf-table.insert) | Inserts at `pos` and shifts following sequence elements; omitting `pos` appends. Returns no values. | Length and insertion positions are bounded by managed integer indices. A host-frozen table rejects mutation because its published contents are immutable. | From aa7c5c2617faf95e3275a46ed537014055e07020 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 03:43:09 +0900 Subject: [PATCH 3/7] fix: align standard library APIs with Lua 5.2 Coerce numeric error objects at positive levels and accept Lua numeric conversions in seek offsets and exit codes. Match iterator termination result counts and newline-terminate all package.config markers. Correct traceback message handling, default levels, frame skipping, and numeric formatting. Complete date week/ISO-year and C-locale conversions, including valid E/O modifiers. Preserve timezone offsets and use the configured TimeProvider timezone consistently for os.date/os.time. Add focused regression coverage and update the API reference, retaining explicit documentation of C runtime and managed timezone-name differences. Validation: 3,389 runtime tests pass on both net8.0 and net10.0; all 73 source-generator tests pass. All four library targets build without warnings. CSharpier, documentation coverage/links, and native Lua 5.2 comparisons pass. Representative regressions fail against the baseline. Paired net10.0 benchmarks against 7faa7f5 show date-field formatting 8-10% faster with 75% fewer allocated bytes; composite date formatting allocates 69% less. Seek/line iteration remain comparable and ipairs is within about 1%, with no additional allocations in these loops. --- docs/stdlib/basic.md | 4 +- docs/stdlib/debug.md | 9 +- docs/stdlib/io.md | 6 +- docs/stdlib/os.md | 21 +-- docs/stdlib/package.md | 2 +- src/Lua/Runtime/Tracebacks.cs | 2 +- src/Lua/Standard/BasicLibrary.cs | 6 +- src/Lua/Standard/DebugLibrary.cs | 22 ++- src/Lua/Standard/FileHandle.cs | 9 +- src/Lua/Standard/IOLibrary.cs | 6 +- src/Lua/Standard/Internal/DateTimeHelper.cs | 93 +++++++--- src/Lua/Standard/Internal/IOHelper.cs | 16 +- src/Lua/Standard/OpenLibsExtensions.cs | 2 +- src/Lua/Standard/OperatingSystemLibrary.cs | 42 ++--- tests/Lua.Tests/DateFormatTests.cs | 172 ++++++++++++++++++ tests/Lua.Tests/DebugTracebackTests.cs | 61 +++++++ tests/Lua.Tests/FileSeekTests.cs | 30 +++ tests/Lua.Tests/IOTests.cs | 30 +++ tests/Lua.Tests/ModuleContractTests.cs | 17 ++ tests/Lua.Tests/OsExitTests.cs | 51 ++++++ .../Lua.Tests/StandardLibraryContractTests.cs | 88 +++++++++ 21 files changed, 594 insertions(+), 95 deletions(-) create mode 100644 tests/Lua.Tests/DateFormatTests.cs create mode 100644 tests/Lua.Tests/OsExitTests.cs diff --git a/docs/stdlib/basic.md b/docs/stdlib/basic.md index 58c811e5..93d02928 100644 --- a/docs/stdlib/basic.md +++ b/docs/stdlib/basic.md @@ -9,10 +9,10 @@ Install with `state.OpenBasicLibrary()`. Functions are globals. The [shared stri | [`assert(v [, message])`](https://www.lua.org/manual/5.2/manual.html#pdf-assert) | Returns all arguments when `v` is truthy; otherwise raises an assertion error. The default message is `assertion failed!`. | The host receives a `LuaAssertionException`. Lua 5.2's message is string-like: a number is converted to text; an arbitrary table is not an assertion error object. | | [`collectgarbage([opt [, arg]])`](https://www.lua.org/manual/5.2/manual.html#pdf-collectgarbage) | Recognizes `collect`, `stop`, `restart`, `count`, `step`, `setpause`, `setstepmul`, `setmajorinc`, `isrunning`, `incremental`, and `generational`. | **Limitation:** every recognized option calls `GC.Collect()` and returns **no values**. `arg` is ignored. Thus `count` does not report memory, `stop` does not stop GC, and tuning/mode options do not tune it. The implementation delegates to .NET GC without implementing Lua's collector control protocol. | | [`dofile([filename])`](https://www.lua.org/manual/5.2/manual.html#pdf-dofile) | Loads and executes a file; returns its results and raises load/runtime failures. | **Limitation:** a filename is required; omitted/nil input does not load standard input. Loading goes through `LuaPlatform.FileSystem`. Paths are decoded to .NET text in byte mode. | -| [`error(message [, level])`](https://www.lua.org/manual/5.2/manual.html#pdf-error) | Raises the supplied error object. `level` selects the source prefix for string errors; `0` suppresses that prefix. | Host-facing errors use `LuaRuntimeException`; stack descriptions include managed frames. **Limitation:** numeric messages retain their number type even at positive levels. C Lua converts them to strings when adding source information; the managed exception preserves the original value. At level 0 both preserve a numeric object. | +| [`error(message [, level])`](https://www.lua.org/manual/5.2/manual.html#pdf-error) | Raises the supplied error object. At positive levels, numbers become strings and string messages receive a source prefix when that level exists. At level 0 or below, the original object is preserved. | Host-facing errors use `LuaRuntimeException`; stack descriptions include managed frames. Numeric text follows `NumberFormat`. | | [`_G`](https://www.lua.org/manual/5.2/manual.html#pdf-_G) | The initial global environment table. | Installed from the state's managed environment. Assigning `_G` does not rewrite a function's `_ENV`. | | [`getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-getmetatable) | Returns the metatable, its protected `__metatable` value, or nil. | Also works with managed userdata. | -| [`ipairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-ipairs) | Uses `__ipairs` when present; otherwise returns an iterator, the table, and `0`, stopping at the first missing integer element. | **Limitation:** directly calling the default iterator with a fractional control value increments that double instead of first converting it to a C integer. At exhaustion it returns two nils rather than one. The ordinary generic-for sequence use is unaffected. `__ipairs` cannot yield across the library callback boundary. | +| [`ipairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-ipairs) | Uses `__ipairs` when present; otherwise returns an iterator, the table, and `0`, stopping at the first missing integer element. The default iterator converts its control value to an integer and returns one nil at exhaustion, as in Lua 5.2. | `__ipairs` cannot yield across the library callback boundary. | | [`load(ld [, source [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-load) | Compiles source/bytecode from a string or a reader function. Returns a function, or `nil, error`; an empty reader result ends input. `mode` defaults to `bt`. Explicit `env`, including nil, initializes the first upvalue. | **Limitation:** reader pieces are collected before parsing, so readers can be called past the point where C Lua would detect a syntax error or finish a binary chunk. This follows the current whole-input compiler interface. Reader callbacks cannot Lua-yield, but C# readers can await. Raw binary strings work in either mode; see [dumping](string.md#dumping-and-loading). | | [`loadfile([filename [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-loadfile) | Compiles a file without running it; returns a function or `nil, error`. Recognizes an initial UTF-8 BOM and first-line `#` comment. | **Limitation:** filename is required; there is no standard-input overload in the Lua library. Uses the configured filesystem, with byte-mode paths decoded to .NET text. | | [`next(t [, index])`](https://www.lua.org/manual/5.2/manual.html#pdf-next) | Returns the next key/value, or nil after the last key. Omitted/nil `index` starts traversal. | Order follows the managed table storage and is unspecified. **Limitation:** an unknown hash key such as `next({}, 'missing')` returns nil instead of C Lua's invalid-key error: the managed lookup treats that missing iteration position as the end. | diff --git a/docs/stdlib/debug.md b/docs/stdlib/debug.md index e6d3565c..b7614ea2 100644 --- a/docs/stdlib/debug.md +++ b/docs/stdlib/debug.md @@ -19,7 +19,7 @@ Install with `state.OpenDebugLibrary()`. Thread-aware functions accept an option | [`debug.setmetatable(value, mt)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setmetatable) | Replaces/removes the actual metatable, bypassing `__metatable` protection; returns the value. | Host-frozen tables still reject changing their metatable because `LuaTable.Freeze()` protects managed storage. **Limitation:** light-userdata object tokens have no case in the managed metatable dispatcher, so setting their metatable has no effect. | | [`debug.setupvalue(f, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setupvalue) | Writes an upvalue and returns its name, or no values for an invalid slot. | Can update managed closure capture slots; their names are empty. Stripped Lua names are also empty, consistently with `getupvalue` and C Lua 5.2.4. | | [`debug.setuservalue(u, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setuservalue) | Writes the first user-value slot and returns the userdata. | **Difference/limitation:** any Lua value is accepted, rather than Lua 5.2's table/nil restriction. Userdata exposing no `UserValues` storage returns nil. This reflects the host-defined `Span` storage API. | -| [`debug.traceback([thread,] [message [, level]])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.traceback) | Builds a traceback; non-string/non-number messages pass through unchanged after level validation. | Formats managed/C# frames and Lua source metadata rather than reproducing C Lua's exact trace text. See [traceback details](#traceback-details) for argument and formatting limitations. | +| [`debug.traceback([thread,] [message [, level]])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.traceback) | Builds a traceback; non-string/non-number messages pass through unchanged without inspecting the level. | Formats managed/C# frames and Lua source metadata rather than reproducing C Lua's exact trace text. See [traceback details](#traceback-details). | | [`debug.upvalueid(f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.upvalueid) | Returns an identity token for an upvalue slot; joined Lua slots share an identity. | Uses weakly associated managed identity objects instead of a native address. Retaining the token does not retain the captured payload; this fits managed GC without exposing movable CLR storage addresses. | | [`debug.upvaluejoin(f1, n1, f2, n2)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.upvaluejoin) | Makes one Lua closure's slot share another Lua closure's upvalue. | Restricted to Lua closures, as in Lua 5.2. Managed capture arrays can be inspected/updated but are not joined by this API. | @@ -33,9 +33,6 @@ Stripped bytecode has no line/local names to recover. Source/line fields and loc ## Traceback details -The current formatter and argument handling have these limitations: +The default level is 1 for the current thread, including when that thread is supplied explicitly, and 0 for a different thread. Explicit levels skip that many frames. Negative levels and levels beyond the stack return only the message and traceback heading, as in C Lua 5.2. -- Explicit negative levels return nil rather than a traceback string. -- Supplying the current thread explicitly defaults to level 0 instead of C Lua's current-thread level 1. -- Numeric messages use the managed value's `ToString`, independently of `NumberFormat`. -- The level is validated before passing through a non-string/non-number message. For example, `debug.traceback({}, 'bad')` errors; C Lua returns the table without inspecting the level. +Numeric messages follow `NumberFormat`. Other non-string/non-number messages are returned unchanged; for example, `debug.traceback({}, 'bad')` returns the table without validating the level. A thread in the first argument position selects the target thread; to pass a thread as the message, supply the target thread first. diff --git a/docs/stdlib/io.md b/docs/stdlib/io.md index 6f9de3d8..401d9d95 100644 --- a/docs/stdlib/io.md +++ b/docs/stdlib/io.md @@ -11,7 +11,7 @@ Install with `state.OpenIOLibrary()`. Operations use `LuaPlatform.FileSystem` an | [`io.close([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.close) | Closes the supplied file, or the default output when no argument is supplied. Explicit nil is an error. | Default console standard handles reject closing. File failures use managed messages/error codes; see below. | | [`io.flush()`](https://www.lua.org/manual/5.2/manual.html#pdf-io.flush) | Flushes the current default output. | Calls the configured stream's flush operation; it is not necessarily an OS file stream. | | [`io.input([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.input) | Gets the default input, selects an open handle, or opens a filename for reading and selects it. | **Host-dependent:** filenames go through the filesystem provider. Omitted/nil input is a getter; invalid/closed handles are rejected before changing the default. | -| [`io.lines([filename, ...formats])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.lines) | Iterates using read formats. A filename creates an automatically closed file; omitted/nil filename uses default input without automatic close. Default format is `*l`. | **Limitation:** at EOF the iterator returns a nil result instead of zero results. Generic-for termination is unchanged, but `select('#', iterator())` can differ. This shares the managed read helper's result convention. Closed default input is rejected when creating the iterator. | +| [`io.lines([filename, ...formats])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.lines) | Iterates using read formats. A filename creates an automatically closed file; omitted/nil filename uses default input without automatic close. Default format is `*l`. A failed first read returns no values; successful earlier formats followed by a failure retain their results and the final nil. | Closed default input is rejected when creating the iterator. Automatic close occurs when the first read fails, including a failed numeric conversion, as in Lua 5.2. | | [`io.open(filename [, mode])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.open) | Opens a file, defaulting to `r`; returns a handle or `nil, message, code` for handled open failures. | String mode, rather than `b`, determines the built-in stream's byte/text encoding. Errors include .NET messages/HResult codes. Custom providers can supply different storage semantics. | | [`io.output([file])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.output) | Gets/selects default output, or opens a filename for writing and selects it. | Switching output flushes the previous open output in this implementation. This is an additional effect of its explicit managed flush policy. Invalid/closed arguments are validated before that flush. It does not redirect `print`, which uses `StandardIO.Output` directly. | | [`io.popen(command [, mode])`](https://www.lua.org/manual/5.2/manual.html#pdf-io.popen) | Officially opens a process pipe. | **Unavailable:** not registered; the built-in I/O abstraction does not implement process pipes. A host can expose its own process/stream binding. | @@ -29,9 +29,9 @@ Install with `state.OpenIOLibrary()`. Operations use `LuaPlatform.FileSystem` an | --- | --- | --- | | [`file:close()`](https://www.lua.org/manual/5.2/manual.html#pdf-file:close) | Closes the underlying stream; reports success or handled I/O failure. | Calls `ILuaStream.CloseAsync`; close behavior belongs to the provider. The default stream flushes and disposes the underlying .NET stream. Lua GC finalizers are not used to implement deterministic close. | | [`file:flush()`](https://www.lua.org/manual/5.2/manual.html#pdf-file:flush) | Flushes pending writes. | Calls the provider's flush operation. | -| [`file:lines(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:lines) | Returns an iterator over the file using the given formats; does not automatically close the handle. | Closed handles fail at iterator creation. **Limitation:** the EOF result count follows the same nil-versus-no-results difference as `io.lines`. | +| [`file:lines(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:lines) | Returns an iterator over the file using the given formats; does not automatically close the handle. A failed first read returns no values, as with `io.lines`. | Closed handles fail at iterator creation. | | [`file:read(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:read) | Returns one value per successful format, stopping at the first failed conversion/EOF with nil. No formats means `*l`. | Default streams decode UTF-8 to UTF-16, while byte-mode streams preserve raw bytes. Counts use the selected representation. Numeric scanning remains byte-based, even in UTF-16 mode. | -| [`file:seek([whence [, offset]])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:seek) | Seeks using `set`, `cur`, or `end`; defaults are `cur, 0`. Requires an integral offset, as in C Lua 5.2.4. Returns the resulting position or handled I/O failure. | Offsets/positions are **bytes** in built-in streams, including UTF-16 mode. Buffered unread bytes are accounted for. If a counted read returned only the high surrogate of a supplementary character, its bytes have already been consumed; a position query retains the pending low surrogate. **Limitation:** the offset uses a strict managed `long` read, so numeric strings such as `'1'` are rejected instead of coerced as in C Lua. Underlying seek support is provider-dependent. | +| [`file:seek([whence [, offset]])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:seek) | Seeks using `set`, `cur`, or `end`; defaults are `cur, 0`. Coerces numeric strings but requires an integral offset representable by a managed `long`. Returns the resulting position or handled I/O failure. | Offsets/positions are **bytes** in built-in streams, including UTF-16 mode. Buffered unread bytes are accounted for. If a counted read returned only the high surrogate of a supplementary character, its bytes have already been consumed; a position query retains the pending low surrogate. Underlying seek support is provider-dependent. | | [`file:setvbuf(mode [, size])`](https://www.lua.org/manual/5.2/manual.html#pdf-file:setvbuf) | Selects `no`, `line`, or `full` buffering policy; returns true when accepted. | **Limitation:** built-in `LuaStream` changes flush policy, not the underlying .NET buffer allocation. `no` flushes each write; `line` flushes writes containing LF; `full` uses a per-write size threshold rather than a cumulative C stdio buffer. `size` does not resize the stream's buffer. Custom providers may implement their own policy. | | [`file:write(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-file:write) | Writes strings/numbers and returns the file on success. | Default streams encode UTF-16 text as UTF-8; byte-mode streams write raw byte values. Numbers use `NumberFormat`. | diff --git a/docs/stdlib/os.md b/docs/stdlib/os.md index c1fa6835..79c8957c 100644 --- a/docs/stdlib/os.md +++ b/docs/stdlib/os.md @@ -7,29 +7,28 @@ Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process ex | API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`os.clock()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.clock) | Returns elapsed CPU time in seconds. | **Host-dependent:** calls `ILuaOsEnvironment.GetTotalProcessorTime`. The default reads the current .NET process's total processor time, including host work outside Lua. | -| [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. | Uses `DateTime`, `TimeProvider`, and invariant .NET formatting rather than C `strftime`. **Limitation:** several conversions are incomplete or differ; see below. | +| [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. | Uses managed date/time formatting and the configured `TimeProvider.LocalTimeZone` rather than C `strftime`/process timezone state. Names and composite formats use the C-locale conventions below. | | [`os.difftime(t2, t1)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns `t2 - t1` in seconds. | Direct double subtraction preserves fractions instead of converting to C `time_t` first. **Limitation:** both arguments are required; C Lua 5.2.4 also accepts an omitted/nil second argument as zero, although the manual lists both arguments. | | [`os.execute([command])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.execute) | Officially runs a shell command or queries shell availability. | **Unsupported operation:** no command/nil returns false; supplying a command raises a not-supported error. The library has no shell-execution implementation. | -| [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitations:** `close` is ignored, so the Lua state is not explicitly disposed before exiting. The numeric branch requires an integral 32-bit Lua number through a strict managed `int` read; fractional numbers and numeric strings are rejected instead of converted by C Lua's integer argument reader. | +| [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. Numeric strings are coerced and fractions truncated toward zero to a managed 32-bit code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitation:** `close` is ignored, so the Lua state is not explicitly disposed before exiting. | | [`os.getenv(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.getenv) | Returns an environment value or nil. | **Host-dependent:** default provider reads process environment variables; custom/Unity providers may expose a managed dictionary instead. Names/results are converted between byte strings and .NET text when needed. | | [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a path or reports failure. | **Limitation:** default `FileSystem.Remove` removes files only, even on POSIX hosts where C Lua can remove empty directories. It uses `File.Exists`/`File.Delete` and has no directory-removal path. Handled errors use .NET HResult codes. | | [`os.rename(oldname, newname)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.rename) | Moves/renames a file or directory; reports success or handled failure. | **Host-dependent:** default provider uses `File.Move`/`File.Replace` and `Directory.Move`. Directory replacement restrictions follow .NET/host filesystem behavior rather than C `rename`; .NET Standard 2.1 uses `File.Replace` for an existing file destination. | | [`os.setlocale(locale [, category])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Officially selects or queries a C locale. | **Limitation:** only the literal `C` is accepted; other strings return nil, nil is not a locale-query operation, and `category` is ignored. No C or .NET locale is changed. Date formatting stays invariant; string casing/classification follows the [string library's rules](string.md). | -| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. `isdst` is ignored. Current time comes from `TimeProvider`; table conversion uses the system local timezone. | +| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. `isdst` is ignored. Current time and the local timezone come from `TimeProvider`, consistently with `os.date`. | | [`os.tmpname()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.tmpname) | Returns a temporary pathname. | **Host-dependent:** default `Path.GetTempFileName` creates an empty file while choosing the name. The caller owns cleanup; unlike `io.tmpfile`, this API does not attach delete-on-close behavior. The name uses the state's string representation. | ## Date conversion details -The date formatter is a managed mapping, not a complete C `strftime` implementation. The following conversions are recognized: +The date formatter implements a managed mapping of [POSIX `strftime` conversions](https://man7.org/linux/man-pages/man3/strftime.3p.html). It uses fixed C-locale forms rather than the host C locale. The following conversions are recognized: `%a %A %b %B %c %C %d %D %e %F %g %G %h %H %I %j %m %M %n %p %r %R %S %t %T %u %U %V %w %W %x %X %y %Y %z %Z %%` -Known formatting differences and incomplete conversions include: +Formatting rules and remaining differences: -- `%U`, `%V`, and `%W` currently emit the literal `??`; week-number calculation is not implemented. -- `%g`/`%G` use the calendar year, not the ISO week-based year defined for those conversions by POSIX `strftime`. Some C Lua builds reject these conversions entirely. -- `%z` and `%Z` both use .NET's `zzz` offset format, including a colon, rather than separate C numeric-offset/timezone-name conversions. With omitted time, the formatter receives a `DateTime` without timezone-kind information, so even a leading `!` can produce the system local offset in these two conversions. -- `%c`, `%x`, `%X`, month/day names, and related patterns use invariant .NET formatting; changing `os.setlocale` has no effect. In particular, `%c` maps to .NET's long-date/short-time pattern and omits seconds; `%x` uses its short-date pattern; `%r` uses an unpadded 12-hour clock hour. -- `%E`/`%O` modifiers and unknown conversions raise an error. +- `%U` and `%W` count weeks starting on Sunday and Monday respectively, with days before the first such day in week 00. `%V`, `%g`, and `%G` use the ISO week number and week-based year. These POSIX conversions are available here even when a non-POSIX C Lua build rejects them. +- `%z` emits a signed offset as `HHMM`, without a colon. `%Z` emits `UTC` for UTC and the configured timezone's .NET standard/daylight name otherwise; these names can differ from C runtime abbreviations. Both omitted and explicit timestamps use the same timezone rules. +- Month/day names are invariant English. `%c` uses `ddd MMM dd HH:mm:ss yyyy` with a space-padded day, `%x` uses `MM/dd/yy`, `%X` uses `HH:mm:ss`, and `%r` uses `hh:mm:ss AM/PM`. These follow POSIX C-locale forms; a particular C runtime's locale-dependent composites can differ. Changing `os.setlocale` has no effect. +- Valid POSIX `%E`/`%O` modifiers use the same C-locale conversions as their unmodified forms: `%E` accepts `cCxXyY`, and `%O` accepts `deHImMSuUVwWy`. Unknown conversions and invalid/incomplete modifier pairs raise an error. -Timestamp conversion is bounded by .NET date/time ranges. For byte-string states, format literals are represented as byte-string units; the formatter's generated names come from invariant .NET date strings. Use the official link for the complete Lua/C contract and this list for the implemented behavior. +Timestamp conversion is bounded by .NET date/time ranges. For byte-string states, format literals remain byte-string units and managed timezone names are encoded as text. Use the official link for the complete Lua/C contract and this list for the implemented behavior. diff --git a/docs/stdlib/package.md b/docs/stdlib/package.md index c8996bb6..6f07703b 100644 --- a/docs/stdlib/package.md +++ b/docs/stdlib/package.md @@ -7,7 +7,7 @@ Install with `state.OpenModuleLibrary()`. This installs global `require` and the | API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`require(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-require) | Returns a truthy cached module or searches for a loader. Calls the loader with `name` and searcher data; caches a non-nil result, or uses the loaded entry/default `true`. A false cache entry allows a later reload. | **Host-dependent:** a configured `ILuaModuleLoader` is consulted before package searchers when it reports the module exists. This lets C# supply resources/assets without native filesystem loading. The package object is captured when the library is opened, so replacing global `package` does not retarget an existing `require`. Loader/searcher callbacks cannot Lua-yield, but host loaders can await. | -| [`package.config`](https://www.lua.org/manual/5.2/manual.html#pdf-package.config) | A newline-delimited string describing directory, path-list, substitution, executable-directory, and ignore markers. | The directory character is the host .NET separator; the remaining markers are `;`, `?`, `!`, and `-`. **Limitation:** the initializer omits C Lua's final newline, so a parser requiring a newline after every marker can miss the last one. | +| [`package.config`](https://www.lua.org/manual/5.2/manual.html#pdf-package.config) | Five newline-terminated markers describing directory, path-list, substitution, executable-directory, and ignore markers, including the final newline. | The directory character is the host .NET separator; the remaining markers are `;`, `?`, `!`, and `-`. | | [`package.cpath`](https://www.lua.org/manual/5.2/manual.html#pdf-package.cpath) | Officially configures native C module search paths. | **Unavailable:** no field or native C-module searcher is installed. Lua-CSharp uses managed bindings and host module loaders rather than Lua's C ABI. | | [`package.loaded`](https://www.lua.org/manual/5.2/manual.html#pdf-package.loaded) | The table used to cache module values. | Backed by the state's managed loaded-module table. As in Lua 5.2, replacing the `package.loaded` field does not replace the cache used by `require`; mutate its entries instead. | | [`package.loadlib(libname, funcname)`](https://www.lua.org/manual/5.2/manual.html#pdf-package.loadlib) | Officially loads a native library and its Lua C entry point. | **Unavailable:** not registered. A native Lua C function cannot run directly against Lua-CSharp's managed state/stack representation. Expose C# functions or an `ILuaModuleLoader` instead. | diff --git a/src/Lua/Runtime/Tracebacks.cs b/src/Lua/Runtime/Tracebacks.cs index f36ce96c..e496d05b 100644 --- a/src/Lua/Runtime/Tracebacks.cs +++ b/src/Lua/Runtime/Tracebacks.cs @@ -154,7 +154,7 @@ internal static string CreateTracebackMessage( using var list = new PooledList(64); if (message.Type is not LuaValueType.Nil) { - list.AddRange(message.ToString()); + list.AddRange(message.ToLuaString(globalState.Platform.NumberFormat)); list.AddRange("\n"); } diff --git a/src/Lua/Standard/BasicLibrary.cs b/src/Lua/Standard/BasicLibrary.cs index abad4e90..14906bbc 100644 --- a/src/Lua/Standard/BasicLibrary.cs +++ b/src/Lua/Standard/BasicLibrary.cs @@ -62,7 +62,7 @@ public BasicLibrary() (context, cancellationToken) => { var table = context.GetArgument(0); - var i = context.GetArgument(1); + var i = Math.Truncate(context.GetArgument(1)); i++; if (table.TryGetValue(i, out var value)) @@ -71,7 +71,7 @@ public BasicLibrary() } else { - return new(context.Return(LuaValue.Nil, LuaValue.Nil)); + return new(context.Return(LuaValue.Nil)); } } ); @@ -154,6 +154,8 @@ CancellationToken cancellationToken { var value = context.ArgumentCount == 0 ? LuaValue.Nil : context.Arguments[0]; var level = context.HasArgument(1) ? context.GetIntegerArgument(1) : 1; + if (level > 0 && value.Type == LuaValueType.Number) + value = value.ToLuaString(context.State.Platform.NumberFormat); throw new LuaRuntimeException(context.State, value, level); } diff --git a/src/Lua/Standard/DebugLibrary.cs b/src/Lua/Standard/DebugLibrary.cs index 8b165c0a..d85a588f 100644 --- a/src/Lua/Standard/DebugLibrary.cs +++ b/src/Lua/Standard/DebugLibrary.cs @@ -372,19 +372,23 @@ CancellationToken cancellationToken var state = GetLuaThread(context, out var argOffset); var message = context.GetArgumentOrDefault(argOffset); - var level = - context.HasArgument(argOffset + 1) ? context.GetIntegerArgument(argOffset + 1) - : argOffset == 0 ? 1 - : 0; - if (message.Type is not (LuaValueType.Nil or LuaValueType.String or LuaValueType.Number)) { return new(context.Return(message)); } + var level = + context.HasArgument(argOffset + 1) ? context.GetIntegerArgument(argOffset + 1) + : state == context.State ? 1 + : 0; + if (level < 0) { - return new(context.Return(LuaValue.Nil)); + return new( + context.Return( + Runtime.Traceback.CreateTracebackMessage(context.GlobalState, [], message) + ) + ); } if (state is { IsCoroutine: true, LuaTraceback: { } traceback }) @@ -402,15 +406,13 @@ CancellationToken cancellationToken } var callStack = state.GetCallStackFrames(); - var skipCount = Math.Min(Math.Max(level - 1, 0), callStack.Length); - var frames = callStack[..^skipCount]; return new( context.Return( Runtime.Traceback.CreateTracebackMessage( context.GlobalState, - frames, + callStack, message, - level == 1 ? 1 : 0 + level ) ) ); diff --git a/src/Lua/Standard/FileHandle.cs b/src/Lua/Standard/FileHandle.cs index 8cd9de93..fdea414d 100644 --- a/src/Lua/Standard/FileHandle.cs +++ b/src/Lua/Standard/FileHandle.cs @@ -249,7 +249,14 @@ static async (context, cancellationToken) => { var file = context.GetArgument(0); var whence = context.HasArgument(1) ? context.GetStringArgument(1) : "cur"; - var offset = context.HasArgument(2) ? context.GetArgument(2) : 0; + var number = context.HasArgument(2) ? context.GetArgument(2) : 0; + if ( + number < long.MinValue + || number >= 9223372036854775808d + || number != Math.Truncate(number) + ) + LuaRuntimeException.BadArgumentNumberIsNotInteger(context.State, 3); + var offset = (long)number; if (whence is not ("set" or "cur" or "end")) { diff --git a/src/Lua/Standard/IOLibrary.cs b/src/Lua/Standard/IOLibrary.cs index ddf7f05d..29c6ae35 100644 --- a/src/Lua/Standard/IOLibrary.cs +++ b/src/Lua/Standard/IOLibrary.cs @@ -180,7 +180,6 @@ static async (context, cancellationToken) => var upValues = context.GetCsClosure()!.UpValues; var file = upValues[0].Read(); var formats = upValues.AsMemory(1); - var stack = context.State.Stack; context.Return(); var resultCount = await IOHelper.ReadAsync( context.State, @@ -191,10 +190,7 @@ static async (context, cancellationToken) => true, cancellationToken ); - if ( - resultCount > 0 - && stack.Get(context.ReturnFrameBase).Type is LuaValueType.Nil - ) + if (resultCount == 0) { await file.Close(cancellationToken); } diff --git a/src/Lua/Standard/Internal/DateTimeHelper.cs b/src/Lua/Standard/Internal/DateTimeHelper.cs index 01afc7f1..0eff3a98 100644 --- a/src/Lua/Standard/Internal/DateTimeHelper.cs +++ b/src/Lua/Standard/Internal/DateTimeHelper.cs @@ -32,11 +32,9 @@ public static DateTime FromUnixTime(double unixTime) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double GetUnixTimeFromLocalTime(DateTime dateTime) + public static double GetUnixTimeFromLocalTime(DateTime dateTime, TimeZoneInfo zone) { - return new DateTimeOffset( - DateTime.SpecifyKind(dateTime, DateTimeKind.Local) - ).ToUnixTimeSeconds(); + return new DateTimeOffset(dateTime, zone.GetUtcOffset(dateTime)).ToUnixTimeSeconds(); } public static DateTime ParseTimeTable(LuaState state, LuaTable table) @@ -79,9 +77,14 @@ static int GetTimeField( return new(year, month, day, hour, min, sec); } - public static string StrFTime(LuaState state, ReadOnlySpan format, DateTime d) + public static string StrFTime( + LuaState state, + ReadOnlySpan format, + DateTimeOffset time, + TimeZoneInfo zone + ) { - // reference: http://www.cplusplus.com/reference/ctime/strftime/ + var d = time.DateTime; [MethodImpl(MethodImplOptions.NoInlining)] static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan format) @@ -101,33 +104,29 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f 'A' => "dddd", 'b' => "MMM", 'B' => "MMMM", - 'c' => "f", 'd' => "dd", 'D' => "MM/dd/yy", 'F' => "yyyy-MM-dd", - 'g' => "yy", - 'G' => "yyyy", 'h' => "MMM", 'H' => "HH", 'I' => "hh", 'm' => "MM", 'M' => "mm", 'p' => "tt", - 'r' => "h:mm:ss tt", + 'r' => "hh:mm:ss tt", 'R' => "HH:mm", 'S' => "ss", 'T' => "HH:mm:ss", 'y' => "yy", 'Y' => "yyyy", - 'x' => "d", - 'X' => "T", - 'z' => "zzz", - 'Z' => "zzz", + 'x' => "MM/dd/yy", + 'X' => "HH:mm:ss", _ => null, }; } ValueStringBuilder builder = new(); + Span formatBuffer = stackalloc char[32]; var isEscapeSequence = false; @@ -158,7 +157,10 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f if (c is 'O' or 'E') { - ThrowInvalidConversionSpecifier(state, format); + var conversions = c == 'E' ? "cCxXyY" : "deHImMSuUVwWy"; + if (++i >= format.Length || conversions.IndexOf(format[i]) < 0) + ThrowInvalidConversionSpecifier(state, format); + c = format[i]; } isEscapeSequence = false; @@ -166,11 +168,24 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f var pattern = STANDARD_PATTERNS(c); if (pattern != null) { - builder.Append(d.ToString(pattern, CultureInfo.InvariantCulture)); + d.TryFormat(formatBuffer, out var written, pattern, CultureInfo.InvariantCulture); + builder.Append(formatBuffer[..written]); } else switch (c) { + case 'c': + d.TryFormat( + formatBuffer, + out var written, + "ddd MMM dd HH:mm:ss yyyy", + CultureInfo.InvariantCulture + ); + // C-locale %c space-pads a single-digit day. + if (d.Day < 10) + formatBuffer[8] = ' '; + builder.Append(formatBuffer[..written]); + break; case 'e': { var s = d.ToString("%d", CultureInfo.InvariantCulture); @@ -184,8 +199,7 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f builder.Append('\t'); break; case 'C': - // TODO: reduce allocation - builder.Append((d.Year / 100).ToString(CultureInfo.InvariantCulture)); + builder.Append((d.Year / 100).ToString("00", CultureInfo.InvariantCulture)); break; case 'j': builder.Append(d.DayOfYear.ToString("000", CultureInfo.InvariantCulture)); @@ -208,12 +222,45 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f break; } case 'U': - // ISO 8601 week number (00-53) - case 'V': - // Week number with the first Monday as the first day of week one (00-53) case 'W': - // Week number with the first Sunday as the first day of week one (00-53) - builder.Append("??"); + { + var firstDay = c == 'U' ? 0 : 1; + var weekDay = ((int)d.DayOfWeek + 7 - firstDay) % 7; + var week = (d.DayOfYear + 6 - weekDay) / 7; + builder.Append(week.ToString("00", CultureInfo.InvariantCulture)); + break; + } + case 'V': + builder.Append( + ISOWeek.GetWeekOfYear(d).ToString("00", CultureInfo.InvariantCulture) + ); + break; + case 'g': + case 'G': + { + var year = ISOWeek.GetYear(d); + builder.Append( + (c == 'g' ? year % 100 : year).ToString( + c == 'g' ? "00" : "0000", + CultureInfo.InvariantCulture + ) + ); + break; + } + case 'z': + builder.Append(time.Offset < TimeSpan.Zero ? '-' : '+'); + var offset = time.Offset.Duration(); + builder.Append(offset.Hours.ToString("00", CultureInfo.InvariantCulture)); + builder.Append(offset.Minutes.ToString("00", CultureInfo.InvariantCulture)); + break; + case 'Z': + builder.Append( + state.Platform.EncodeText( + zone == TimeZoneInfo.Utc ? "UTC" + : zone.IsDaylightSavingTime(time) ? zone.DaylightName + : zone.StandardName + ) + ); break; default: ThrowInvalidConversionSpecifier(state, format); diff --git a/src/Lua/Standard/Internal/IOHelper.cs b/src/Lua/Standard/Internal/IOHelper.cs index 0f64d3e6..0e8e7310 100644 --- a/src/Lua/Standard/Internal/IOHelper.cs +++ b/src/Lua/Standard/Internal/IOHelper.cs @@ -142,7 +142,7 @@ CancellationToken cancellationToken stack.Push(number ?? LuaValue.Nil); if (number == null) { - return i + 1; + goto ReadFailed; } break; case "*a": @@ -155,7 +155,7 @@ CancellationToken cancellationToken stack.Push(line ?? LuaValue.Nil); if (line == null) { - return i + 1; + goto ReadFailed; } break; case "L": @@ -164,7 +164,7 @@ CancellationToken cancellationToken stack.Push(text == null ? LuaValue.Nil : text); if (text == null) { - return i + 1; + goto ReadFailed; } break; default: @@ -190,7 +190,7 @@ CancellationToken cancellationToken if (ret == null) { stack.Push(default); - return i + 1; + goto ReadFailed; } else { @@ -209,6 +209,14 @@ CancellationToken cancellationToken } return formats.Length; + + ReadFailed: + if (throwError && stack.Get(top).Type == LuaValueType.Nil) + { + stack.PopUntil(top); + return 0; + } + return stack.Count - top; } catch (IOException ex) { diff --git a/src/Lua/Standard/OpenLibsExtensions.cs b/src/Lua/Standard/OpenLibsExtensions.cs index 0d3937f6..6ffca472 100644 --- a/src/Lua/Standard/OpenLibsExtensions.cs +++ b/src/Lua/Standard/OpenLibsExtensions.cs @@ -101,7 +101,7 @@ public static void OpenModuleLibrary(this LuaState state) package["searchers"] = searchers; package["path"] = "?.lua"; package["searchpath"] = moduleLibrary.SearchPathFunction; - package["config"] = $"{Path.DirectorySeparatorChar}\n;\n?\n!\n-"; + package["config"] = $"{Path.DirectorySeparatorChar}\n;\n?\n!\n-\n"; globalState.Environment["package"] = package; globalState.LoadedModules["package"] = package; globalState.Environment["require"] = new CSharpClosure( diff --git a/src/Lua/Standard/OperatingSystemLibrary.cs b/src/Lua/Standard/OperatingSystemLibrary.cs index 51d3433f..bfc38dfe 100644 --- a/src/Lua/Standard/OperatingSystemLibrary.cs +++ b/src/Lua/Standard/OperatingSystemLibrary.cs @@ -51,23 +51,12 @@ CancellationToken cancellationToken format = format[1..]; } - DateTime now; - if (context.HasArgument(1)) - { - var time = context.GetArgument(1); - now = DateTimeHelper.FromUnixTime(time); - if (!useUtc) - { - now = now.ToLocalTime(); - } - } - else - { - var provider = context.GlobalState.Platform.TimeProvider; - now = useUtc ? provider.GetUtcNow().DateTime : provider.GetLocalNow().DateTime; - } - - var isDst = !useUtc && now.IsDaylightSavingTime(); + var provider = context.GlobalState.Platform.TimeProvider; + var zone = useUtc ? TimeZoneInfo.Utc : provider.LocalTimeZone; + var now = context.HasArgument(1) + ? new DateTimeOffset(DateTimeHelper.FromUnixTime(context.GetArgument(1))) + : provider.GetUtcNow(); + now = TimeZoneInfo.ConvertTime(now, zone); if (format is "*t") { @@ -81,14 +70,14 @@ CancellationToken cancellationToken ["sec"] = now.Second, ["wday"] = (int)now.DayOfWeek + 1, ["yday"] = now.DayOfYear, - ["isdst"] = isDst, + ["isdst"] = zone.IsDaylightSavingTime(now), }; return new(context.Return(table)); } else { - return new(context.Return(DateTimeHelper.StrFTime(context.State, format, now))); + return new(context.Return(DateTimeHelper.StrFTime(context.State, format, now, zone))); } } @@ -134,13 +123,9 @@ CancellationToken cancellationToken { exitCode = b ? 0 : 1; } - else if (code.TryRead(out var d)) - { - exitCode = d; - } else { - LuaRuntimeException.BadArgument(context.State, 1, LuaValueType.Nil, code.Type); + exitCode = context.GetIntegerArgument(0); } } @@ -234,7 +219,14 @@ CancellationToken cancellationToken { var table = context.GetArgument(0); var date = DateTimeHelper.ParseTimeTable(context.State, table); - return new(context.Return(DateTimeHelper.GetUnixTimeFromLocalTime(date))); + return new( + context.Return( + DateTimeHelper.GetUnixTimeFromLocalTime( + date, + context.State.Platform.TimeProvider.LocalTimeZone + ) + ) + ); } else { diff --git a/tests/Lua.Tests/DateFormatTests.cs b/tests/Lua.Tests/DateFormatTests.cs new file mode 100644 index 00000000..11a51620 --- /dev/null +++ b/tests/Lua.Tests/DateFormatTests.cs @@ -0,0 +1,172 @@ +using Lua.Platforms; +using Lua.Standard; +using Microsoft.Extensions.Time.Testing; + +namespace Lua.Tests; + +[TestFixture(LuaStringMode.Utf16)] +[TestFixture(LuaStringMode.Bytes)] +public class DateFormatTests(LuaStringMode mode) +{ + LuaState CreateState(FakeTimeProvider provider) + { + var platform = + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default; + var state = LuaState.Create(platform with { TimeProvider = provider }); + state.OpenStandardLibraries(); + return state; + } + + [TestCase(2016, 1, 1, "00 00 53 15 2015")] + [TestCase(2016, 1, 3, "01 00 53 15 2015")] + [TestCase(2016, 1, 4, "01 01 01 16 2016")] + [TestCase(2017, 1, 1, "01 00 52 16 2016")] + [TestCase(2018, 12, 31, "52 53 01 19 2019")] + [TestCase(2020, 12, 31, "52 52 53 20 2020")] + [TestCase(2021, 1, 1, "00 00 53 20 2020")] + public async Task WeekNumbersAndIsoYearsRespectYearBoundaries( + int year, + int month, + int day, + string expected + ) + { + var provider = new FakeTimeProvider( + new DateTimeOffset(year, month, day, 12, 0, 0, TimeSpan.Zero) + ); + using var state = CreateState(provider); + var result = await state.DoStringAsync("return os.date('!%U %W %V %g %G')"); + Assert.That(result, Is.EqualTo(new LuaValue[] { expected })); + } + + [Test] + public async Task CompositeFormatsUseCLocaleFieldsAndPadding() + { + var provider = new FakeTimeProvider(new DateTimeOffset(2000, 1, 2, 3, 4, 5, TimeSpan.Zero)); + using var state = CreateState(provider); + var result = await state.DoStringAsync("return os.date('!%c|%x|%X|%r|%C|%%')"); + Assert.That( + result, + Is.EqualTo( + new LuaValue[] { "Sun Jan 2 03:04:05 2000|01/02/00|03:04:05|03:04:05 AM|20|%" } + ) + ); + } + + [TestCase(1, "00")] + [TestCase(999, "09")] + [TestCase(1000, "10")] + public async Task CenturyIsPaddedToTwoDigits(int year, string expected) + { + var provider = new FakeTimeProvider(new DateTimeOffset(year, 1, 1, 0, 0, 0, TimeSpan.Zero)); + using var state = CreateState(provider); + var result = await state.DoStringAsync("return os.date('!%C')"); + Assert.That(result, Is.EqualTo(new LuaValue[] { expected })); + } + + [TestCase(1, "-0800 Test Standard", false, 4)] + [TestCase(7, "-0700 Test Daylight", true, 5)] + public async Task LocalAndUtcDatesUseTheConfiguredZoneForCurrentAndExplicitTimes( + int month, + string expected, + bool daylight, + int hour + ) + { + var rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule( + new DateTime(2000, 1, 1), + new DateTime(2099, 12, 31), + TimeSpan.FromHours(1), + TimeZoneInfo.TransitionTime.CreateFloatingDateRule( + new DateTime(1, 1, 1, 2, 0, 0), + 3, + 2, + DayOfWeek.Sunday + ), + TimeZoneInfo.TransitionTime.CreateFloatingDateRule( + new DateTime(1, 1, 1, 2, 0, 0), + 11, + 1, + DayOfWeek.Sunday + ) + ); + var zone = TimeZoneInfo.CreateCustomTimeZone( + "Test/Pacific", + TimeSpan.FromHours(-8), + "Test Pacific", + "Test Standard", + "Test Daylight", + [rule] + ); + var provider = new FakeTimeProvider( + new DateTimeOffset(2020, month, 15, 12, 0, 0, TimeSpan.Zero) + ); + provider.SetLocalTimeZone(zone); + using var state = CreateState(provider); + state.Environment["timestamp"] = provider.GetUtcNow().ToUnixTimeSeconds(); + var result = await state.DoStringAsync( + """ + local current=os.date('*t') + local explicit=os.date('*t',timestamp) + return os.date('%z %Z'),os.date('%z %Z',timestamp), + current.isdst,explicit.isdst,current.hour,explicit.hour, + os.date('!%z %Z'),os.date('!%z %Z',timestamp),os.date('!*t').isdst, + os.time(current)==timestamp,os.time(explicit)==timestamp + """ + ); + Assert.That( + result, + Is.EqualTo( + new LuaValue[] + { + expected, + expected, + daylight, + daylight, + hour, + hour, + "+0000 UTC", + "+0000 UTC", + false, + true, + true, + } + ) + ); + } + + [Test] + public async Task NumericOffsetsIncludeMinutesWithoutAColon() + { + var provider = new FakeTimeProvider(); + provider.SetLocalTimeZone( + TimeZoneInfo.CreateCustomTimeZone( + "Test/Nepal", + TimeSpan.FromMinutes(345), + "Test Nepal", + "Test Nepal" + ) + ); + using var state = CreateState(provider); + var result = await state.DoStringAsync("return os.date('%z')"); + Assert.That(result, Is.EqualTo(new LuaValue[] { "+0545" })); + } + + [Test] + public async Task AlternativeModifiersUseTheSameCLocaleConversions() + { + using var state = CreateState(new FakeTimeProvider()); + await state.DoStringAsync( + """ + for _,set in ipairs({{'E','cCxXyY'},{'O','deHImMSuUVwWy'}}) do + for conversion in set[2]:gmatch('.') do + assert(os.date('!%'..set[1]..conversion)==os.date('!%'..conversion)) + end + end + assert(not pcall(os.date,'!%Ea')) + assert(not pcall(os.date,'!%Oc')) + assert(not pcall(os.date,'!%OY')) + """ + ); + } +} diff --git a/tests/Lua.Tests/DebugTracebackTests.cs b/tests/Lua.Tests/DebugTracebackTests.cs index 551cb87f..c7ac72a3 100644 --- a/tests/Lua.Tests/DebugTracebackTests.cs +++ b/tests/Lua.Tests/DebugTracebackTests.cs @@ -7,6 +7,67 @@ namespace Lua.Tests; [TestFixture(LuaStringMode.Bytes)] public class DebugTracebackTests(LuaStringMode mode) { + [TestCase(LuaNumberFormat.DotNet)] + [TestCase(LuaNumberFormat.Lua)] + public async Task TracebackHandlesMessagesAndNegativeLevelsBeforeFormatting( + LuaNumberFormat format + ) + { + var platform = + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default; + using var state = LuaState.Create(platform with { NumberFormat = format }); + state.OpenStandardLibraries(); + await state.DoStringAsync( + """ + local co=coroutine.create(function() end) + for _,object in ipairs({{},false,function() end}) do + assert(debug.traceback(object,'invalid')==object) + assert(debug.traceback(co,object,'invalid')==object) + end + assert(debug.traceback(co,co,'invalid')==co) + local message=tostring(1/3)..'\nstack traceback:' + assert(debug.traceback(1/3,-1)==message) + assert(debug.traceback(co,1/3,-1)==message) + assert(debug.traceback(nil,-1)=='stack traceback:') + assert(debug.traceback(nil,2147483647)=='stack traceback:') + """ + ); + } + + [Test] + public async Task CurrentThreadDefaultsAndExplicitLevelsSkipTheSameFrames() + { + using var state = LuaState.Create( + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default + ); + state.OpenStandardLibraries(); + await state.DoStringAsync( + """ + local function leaf() + local co=coroutine.running() + local implicit,explicit=debug.traceback(),debug.traceback(co) + assert(implicit==explicit) + local traces={} + for level=0,5 do + local a,b=debug.traceback(nil,level),debug.traceback(co,nil,level) + assert(a==b) + traces[level]=a + end + assert(traces[0]:find("in function 'debug.traceback'",1,true)) + assert(traces[1]:find("'leaf'",1,true) and not traces[2]:find("'leaf'",1,true)) + assert(traces[2]:find("'parent'",1,true) and not traces[3]:find("'parent'",1,true)) + assert(traces[3]:find("'outer'",1,true) and not traces[4]:find("'outer'",1,true)) + assert(traces[5]=='stack traceback:') + return true + end + local function parent() local result=leaf();return result end + local function outer() local result=parent();return result end + assert(outer()) + """, + "@traceback-levels.lua" + ); + } + [TestCase("failed")] [TestCase("completed")] [TestCase("unstarted")] diff --git a/tests/Lua.Tests/FileSeekTests.cs b/tests/Lua.Tests/FileSeekTests.cs index 14780314..c9e517e2 100644 --- a/tests/Lua.Tests/FileSeekTests.cs +++ b/tests/Lua.Tests/FileSeekTests.cs @@ -5,6 +5,36 @@ namespace Lua.Tests; public class FileSeekTests { + [TestCase("'3'", 3d)] + [TestCase("'3000000000'", 3000000000d)] + [TestCase("'0x10'", 16d)] + public async Task LuaSeekCoercesNumericStrings(string offset, double expected) + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + using var stream = new PositionStream(); + state.Environment["file"] = new LuaValue(new FileHandle(stream)); + var result = await state.DoStringAsync($"return file:seek('set',{offset})"); + Assert.That(result, Is.EqualTo(new LuaValue[] { expected })); + } + + [TestCase("1.5")] + [TestCase("'1.5'")] + [TestCase("0/0")] + [TestCase("math.huge")] + [TestCase("-math.huge")] + [TestCase("9223372036854775808")] + public async Task LuaSeekRejectsFractionalAndUnrepresentableOffsets(string offset) + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + using var stream = new PositionStream(); + state.Environment["file"] = new LuaValue(new FileHandle(stream)); + await state.DoStringAsync( + $"local ok,e=pcall(file.seek,file,'set',{offset});assert(not ok and e:find('integer',1,true))" + ); + } + [Test] public async Task LuaSeekAcceptsOffsetsBeyondInt32() { diff --git a/tests/Lua.Tests/IOTests.cs b/tests/Lua.Tests/IOTests.cs index 436e0298..55816530 100644 --- a/tests/Lua.Tests/IOTests.cs +++ b/tests/Lua.Tests/IOTests.cs @@ -30,6 +30,36 @@ string GetTestFilePath(string filename) return Path.Combine(testDirectory, filename); } + [TestCase("12", "'*n','*n'")] + [TestCase("word", "'*n'")] + [TestCase("", "'*l'")] + public async Task FilenameLinesReturnsNoValuesAndClosesAtTheFirstFailedRead( + string content, + string formats + ) + { + var path = GetTestFilePath("iterator.txt"); + await File.WriteAllTextAsync(path, content); + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + state.Environment["path"] = path; + state.Environment["hasResult"] = content == "12"; + await state.DoStringAsync( + $$""" + local it=io.lines(path,{{formats}}) + local _,file=debug.getupvalue(it,1) + assert(io.type(file)=='file') + if hasResult then + local values=table.pack(it()) + assert(values.n==2 and values[1]==12 and values[2]==nil) + assert(io.type(file)=='file') + end + assert(select('#',it())==0) + assert(io.type(file)=='closed file') + """ + ); + } + [Test] public async Task TextStream_Write_And_Read_Text() { diff --git a/tests/Lua.Tests/ModuleContractTests.cs b/tests/Lua.Tests/ModuleContractTests.cs index 3ccc16fa..e21fe126 100644 --- a/tests/Lua.Tests/ModuleContractTests.cs +++ b/tests/Lua.Tests/ModuleContractTests.cs @@ -9,6 +9,23 @@ namespace Lua.Tests; [TestFixture(LuaStringMode.Bytes)] public class ModuleContractTests(LuaStringMode stringMode) { + [Test] + public async Task PackageConfigContainsFiveNewlineTerminatedMarkers() + { + using var state = CreateState(); + state.Environment["separator"] = Path.DirectorySeparatorChar.ToString(); + await state.DoStringAsync( + """ + assert(package.config==separator..'\n;\n?\n!\n-\n') + local markers={} + for marker in package.config:gmatch('([^\n]*)\n') do + markers[#markers+1]=marker + end + assert(#markers==5 and markers[5]=='-') + """ + ); + } + [TestCase(false)] [TestCase(true)] public async Task StandardLibrariesAreRegisteredForRequire(bool reopen) diff --git a/tests/Lua.Tests/OsExitTests.cs b/tests/Lua.Tests/OsExitTests.cs new file mode 100644 index 00000000..1e69b1b2 --- /dev/null +++ b/tests/Lua.Tests/OsExitTests.cs @@ -0,0 +1,51 @@ +using Lua.Platforms; +using Lua.Standard; + +namespace Lua.Tests; + +public class OsExitTests +{ + [TestCase("", 0)] + [TestCase("nil", 0)] + [TestCase("true", 0)] + [TestCase("false", 1)] + [TestCase("2.9", 2)] + [TestCase("'-2.9'", -2)] + [TestCase("'0x10'", 16)] + public async Task ExitCoercesLuaCodesBeforeCallingTheHost(string argument, int expected) + { + var environment = new ExitEnvironment(); + using var state = LuaState.Create(LuaPlatform.Default with { OsEnvironment = environment }); + state.OpenStandardLibraries(); + await state.DoStringAsync($"pcall(os.exit{(argument.Length == 0 ? "" : "," + argument)})"); + Assert.That(environment.ExitCode, Is.EqualTo(expected)); + } + + [TestCase("{}")] + [TestCase("'invalid'")] + [TestCase("0/0")] + [TestCase("math.huge")] + public async Task InvalidCodesDoNotCallTheHost(string argument) + { + var environment = new ExitEnvironment(); + using var state = LuaState.Create(LuaPlatform.Default with { OsEnvironment = environment }); + state.OpenStandardLibraries(); + await state.DoStringAsync($"assert(not pcall(os.exit,{argument}))"); + Assert.That(environment.ExitCode, Is.Null); + } + + sealed class ExitEnvironment : ILuaOsEnvironment + { + public int? ExitCode { get; private set; } + + public ValueTask Exit(int exitCode, CancellationToken cancellationToken) + { + ExitCode = exitCode; + throw new InvalidOperationException("Exit requested"); + } + + public string? GetEnvironmentVariable(string name) => null; + + public double GetTotalProcessorTime() => 0; + } +} diff --git a/tests/Lua.Tests/StandardLibraryContractTests.cs b/tests/Lua.Tests/StandardLibraryContractTests.cs index 5aa3414a..09280f67 100644 --- a/tests/Lua.Tests/StandardLibraryContractTests.cs +++ b/tests/Lua.Tests/StandardLibraryContractTests.cs @@ -31,6 +31,94 @@ await state.DoStringAsync( ); } + [TestCase(-1)] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [TestCase(10000)] + public async Task ErrorCoercesNumbersOnlyAtPositiveLevels(int level) + { + using var state = CreateState(); + state.Environment["level"] = level; + await state.DoStringAsync( + """ + local function fail() error(42,level) end + local function check(value) + if level>0 then + assert(type(value)=='string' and value:sub(-2)=='42') + else + assert(type(value)=='number' and value==42) + end + return value + end + local ok,value=pcall(fail) + assert(not ok) + check(value) + ok,value=xpcall(fail,check) + assert(not ok) + check(value) + local object={} + ok,value=pcall(error,object,level) + assert(not ok and value==object) + """ + ); + } + + [Test] + public async Task IPairsIteratorTruncatesItsControlAndReturnsOneNilAtExhaustion() + { + using var state = CreateState(); + await state.DoStringAsync( + """ + local it,t=ipairs({'first','second'}) + local key,value=it(t,'0.9') + assert(key==1 and value=='first') + assert(select('#',it(t,1))==2) + assert(select('#',it(t,2))==1 and it(t,2)==nil) + """ + ); + } + + [TestCase("file:lines()")] + [TestCase("file:lines('*L')")] + [TestCase("file:lines(100)")] + [TestCase("io.lines()")] + [TestCase("io.lines(nil, '*n', '*l')")] + [TestCase("file:lines('*n', '*l')")] + public async Task LineIteratorsReturnNoValuesWhenTheFirstReadFails(string expression) + { + using var state = CreateState(); + using var stream = ILuaStream.CreateFromString("12 word\n"); + state.Environment["file"] = new LuaValue(new FileHandle(stream)); + await state.DoStringAsync( + $$""" + io.input(file) + local it={{expression}} + assert(it()~=nil) + assert(select('#',it())==0) + assert(select('#',it())==0) + assert(io.type(file)=='file') + assert(select('#',file:read())==1 and file:read()==nil) + """ + ); + } + + [Test] + public async Task LineIteratorsKeepPartialResultsWhenALaterReadFails() + { + using var state = CreateState(); + using var stream = ILuaStream.CreateFromString("12"); + state.Environment["file"] = new LuaValue(new FileHandle(stream)); + await state.DoStringAsync( + """ + local it=file:lines('*n','*n') + local values=table.pack(it()) + assert(values.n==2 and values[1]==12 and values[2]==nil) + assert(select('#',it())==0) + """ + ); + } + [Test] public async Task AssertCoercesNumericMessagesButRejectsTables() { From 655da9314320dc8f1a8d3826f502d8c615a409f4 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:15:51 +0900 Subject: [PATCH 4/7] fix: preserve date resources and Lua 5.2 traceback behavior Use isdst to resolve repeated local times, including negative daylight adjustments, and truncate explicit timestamps toward zero before date conversion. Dispose the date formatter buffer on both success and failure. Apply Lua 5.2 traceback abbreviation before formatting omitted frames, including its absolute skip-level marker behavior. Keep public C# traces complete and simplify the debug-library dispatch to one formatter call. Add regressions for DST folds, fractional timestamps, all date-format error exits, and deep live/suspended/failed tracebacks. Observe pool returns synchronously without GC assertions, sleeps, retries, or production hooks. Update the API reference and retain the remaining os.time limitations. Validation: 3,431 runtime tests pass on both net8.0 and net10.0, with all 73 generator tests passing. The final no-DST optimization passes all 57 date tests on both targets. All four library targets build without warnings; formatting and documentation coverage/link checks pass. Native Lua 5.2 matches all nine traceback boundary/skip cases and three fractional-time cases in both string modes. All four regression groups fail on aa7c5c2. Paired net10.0 benchmarks against aa7c5c2: a 1,000-call Lua traceback is about 80% faster and allocates 1,584 rather than 66,320 bytes (97.6% less). Ordinary date/time-table workloads remain within about 1.3% of baseline with unchanged allocations. Avoid the isdst lookup in zones without DST. --- docs/stdlib/debug.md | 2 + docs/stdlib/os.md | 6 +- src/Lua/Runtime/Tracebacks.cs | 25 ++++- src/Lua/Standard/DebugLibrary.cs | 33 ++---- src/Lua/Standard/Internal/DateTimeHelper.cs | 18 +++- src/Lua/Standard/OperatingSystemLibrary.cs | 4 +- tests/Lua.Tests/DateBufferTests.cs | 66 ++++++++++++ tests/Lua.Tests/DateFormatTests.cs | 107 +++++++++++++++----- tests/Lua.Tests/DebugTracebackTests.cs | 102 +++++++++++++++++++ 9 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 tests/Lua.Tests/DateBufferTests.cs diff --git a/docs/stdlib/debug.md b/docs/stdlib/debug.md index b7614ea2..ef9dd454 100644 --- a/docs/stdlib/debug.md +++ b/docs/stdlib/debug.md @@ -35,4 +35,6 @@ Stripped bytecode has no line/local names to recover. Source/line fields and loc The default level is 1 for the current thread, including when that thread is supplied explicitly, and 0 for a different thread. Explicit levels skip that many frames. Negative levels and levels beyond the stack return only the message and traceback heading, as in C Lua 5.2. +Deep Lua-facing tracebacks use Lua 5.2's head/tail abbreviation and insert `...` without formatting the omitted frames. The marker follows Lua 5.2's absolute stack-level rule; an explicit starting level past that marker can produce a longer trace. C# `Traceback` APIs retain complete traces. + Numeric messages follow `NumberFormat`. Other non-string/non-number messages are returned unchanged; for example, `debug.traceback({}, 'bad')` returns the table without validating the level. A thread in the first argument position selects the target thread; to pass a thread as the message, supply the target thread first. diff --git a/docs/stdlib/os.md b/docs/stdlib/os.md index 79c8957c..8c6f5fcc 100644 --- a/docs/stdlib/os.md +++ b/docs/stdlib/os.md @@ -7,7 +7,7 @@ Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process ex | API / official reference | Inputs and result | Lua-CSharp implementation notes and differences | | --- | --- | --- | | [`os.clock()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.clock) | Returns elapsed CPU time in seconds. | **Host-dependent:** calls `ILuaOsEnvironment.GetTotalProcessorTime`. The default reads the current .NET process's total processor time, including host work outside Lua. | -| [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. | Uses managed date/time formatting and the configured `TimeProvider.LocalTimeZone` rather than C `strftime`/process timezone state. Names and composite formats use the C-locale conventions below. | +| [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. Explicit timestamps are truncated toward zero to whole Unix seconds, as in C Lua 5.2. | Uses managed date/time formatting and the configured `TimeProvider.LocalTimeZone` rather than C `strftime`/process timezone state. Names and composite formats use the C-locale conventions below. | | [`os.difftime(t2, t1)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns `t2 - t1` in seconds. | Direct double subtraction preserves fractions instead of converting to C `time_t` first. **Limitation:** both arguments are required; C Lua 5.2.4 also accepts an omitted/nil second argument as zero, although the manual lists both arguments. | | [`os.execute([command])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.execute) | Officially runs a shell command or queries shell availability. | **Unsupported operation:** no command/nil returns false; supplying a command raises a not-supported error. The library has no shell-execution implementation. | | [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. Numeric strings are coerced and fractions truncated toward zero to a managed 32-bit code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitation:** `close` is ignored, so the Lua state is not explicitly disposed before exiting. | @@ -15,7 +15,7 @@ Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process ex | [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a path or reports failure. | **Limitation:** default `FileSystem.Remove` removes files only, even on POSIX hosts where C Lua can remove empty directories. It uses `File.Exists`/`File.Delete` and has no directory-removal path. Handled errors use .NET HResult codes. | | [`os.rename(oldname, newname)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.rename) | Moves/renames a file or directory; reports success or handled failure. | **Host-dependent:** default provider uses `File.Move`/`File.Replace` and `Directory.Move`. Directory replacement restrictions follow .NET/host filesystem behavior rather than C `rename`; .NET Standard 2.1 uses `File.Replace` for an existing file destination. | | [`os.setlocale(locale [, category])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Officially selects or queries a C locale. | **Limitation:** only the literal `C` is accepted; other strings return nil, nil is not a locale-query operation, and `category` is ignored. No C or .NET locale is changed. Date formatting stays invariant; string casing/classification follows the [string library's rules](string.md). | -| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. `isdst` is ignored. Current time and the local timezone come from `TimeProvider`, consistently with `os.date`. | +| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. `isdst` disambiguates repeated local times; see below. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. Current time and the local timezone come from `TimeProvider`, consistently with `os.date`. | | [`os.tmpname()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.tmpname) | Returns a temporary pathname. | **Host-dependent:** default `Path.GetTempFileName` creates an empty file while choosing the name. The caller owns cleanup; unlike `io.tmpfile`, this API does not attach delete-on-close behavior. The name uses the state's string representation. | ## Date conversion details @@ -32,3 +32,5 @@ Formatting rules and remaining differences: - Valid POSIX `%E`/`%O` modifiers use the same C-locale conversions as their unmodified forms: `%E` accepts `cCxXyY`, and `%O` accepts `deHImMSuUVwWy`. Unknown conversions and invalid/incomplete modifier pairs raise an error. Timestamp conversion is bounded by .NET date/time ranges. For byte-string states, format literals remain byte-string units and managed timezone names are encoded as text. Use the official link for the complete Lua/C contract and this list for the implemented behavior. + +During a repeated local hour, a truthy `isdst` selects the daylight-saving occurrence and false selects standard time. Nil/omitted `isdst` uses .NET's standard-time default for ambiguous dates. This preserves `os.time(os.date('*t', timestamp))` across the repeated hour, including negative daylight-saving adjustments. **Remaining limitation:** for an unambiguous date, `isdst` does not force a seasonal correction as C `mktime` can; the timezone's applicable offset is used. diff --git a/src/Lua/Runtime/Tracebacks.cs b/src/Lua/Runtime/Tracebacks.cs index e496d05b..4a6bdfcd 100644 --- a/src/Lua/Runtime/Tracebacks.cs +++ b/src/Lua/Runtime/Tracebacks.cs @@ -148,7 +148,8 @@ internal static string CreateTracebackMessage( LuaGlobalState globalState, ReadOnlySpan stackFrames, LuaValue message, - int skipCount = 0 + int skipCount = 0, + bool abbreviate = false ) { using var list = new PooledList(64); @@ -162,8 +163,28 @@ internal static string CreateTracebackMessage( var intFormatBuffer = (stackalloc char[15]); var shortSourceBuffer = (stackalloc char[59]); - for (var index = stackFrames.Length - 1; index >= 0; index--) + const int firstPartSize = 12; + const int lastPartSize = 10; + var firstIndex = stackFrames.Length - 1; + var abbreviationIndex = -1; + if (abbreviate) { + if (firstIndex >= 0 && stackFrames[firstIndex].Function is LuaClosure) + firstIndex--; + // Lua 5.2's marker is at level 11, followed by the oldest 11 frames. + if (firstIndex > firstPartSize + lastPartSize) + abbreviationIndex = firstIndex - firstPartSize + 1; + firstIndex -= Math.Max(skipCount, 0); + skipCount = 0; + } + + for (var index = firstIndex; index >= 0; index--) + { + if (index == abbreviationIndex) + { + list.AddRange("\t...\n"); + index = lastPartSize; + } var lastFunc = stackFrames[index].Function; if (lastFunc is not null and not LuaClosure) { diff --git a/src/Lua/Standard/DebugLibrary.cs b/src/Lua/Standard/DebugLibrary.cs index d85a588f..85b68964 100644 --- a/src/Lua/Standard/DebugLibrary.cs +++ b/src/Lua/Standard/DebugLibrary.cs @@ -382,37 +382,18 @@ CancellationToken cancellationToken : state == context.State ? 1 : 0; - if (level < 0) - { - return new( - context.Return( - Runtime.Traceback.CreateTracebackMessage(context.GlobalState, [], message) - ) - ); - } - - if (state is { IsCoroutine: true, LuaTraceback: { } traceback }) - { - return new( - context.Return( - Runtime.Traceback.CreateTracebackMessage( - context.GlobalState, - traceback.StackFrames, - message, - level - ) - ) - ); - } - - var callStack = state.GetCallStackFrames(); + ReadOnlySpan frames = + level < 0 ? [] + : state is { IsCoroutine: true, LuaTraceback: { } traceback } ? traceback.StackFrames + : state.GetCallStackFrames(); return new( context.Return( Runtime.Traceback.CreateTracebackMessage( context.GlobalState, - callStack, + frames, message, - level + level, + abbreviate: true ) ) ); diff --git a/src/Lua/Standard/Internal/DateTimeHelper.cs b/src/Lua/Standard/Internal/DateTimeHelper.cs index 0eff3a98..d9e11d4a 100644 --- a/src/Lua/Standard/Internal/DateTimeHelper.cs +++ b/src/Lua/Standard/Internal/DateTimeHelper.cs @@ -27,14 +27,22 @@ public static double GetUnixTime(DateTime dateTime, DateTime epoch) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTime FromUnixTime(double unixTime) { - var ts = TimeSpan.FromSeconds(unixTime); + var ts = TimeSpan.FromSeconds(Math.Truncate(unixTime)); return DateTime.UnixEpoch + ts; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double GetUnixTimeFromLocalTime(DateTime dateTime, TimeZoneInfo zone) + public static double GetUnixTimeFromLocalTime(DateTime dateTime, TimeZoneInfo zone, bool isDst) { - return new DateTimeOffset(dateTime, zone.GetUtcOffset(dateTime)).ToUnixTimeSeconds(); + var offset = zone.GetUtcOffset(dateTime); + // GetUtcOffset already selects standard time for an ambiguous clock reading. + if (isDst && zone.IsAmbiguousTime(dateTime)) + { + var offsets = zone.GetAmbiguousTimeOffsets(dateTime); + var first = new DateTimeOffset(dateTime, offsets[0]); + offset = zone.IsDaylightSavingTime(first) ? offsets[0] : offsets[1]; + } + return new DateTimeOffset(dateTime, offset).ToUnixTimeSeconds(); } public static DateTime ParseTimeTable(LuaState state, LuaTable table) @@ -125,7 +133,7 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f }; } - ValueStringBuilder builder = new(); + using ValueStringBuilder builder = new(); Span formatBuffer = stackalloc char[32]; var isEscapeSequence = false; @@ -273,6 +281,6 @@ static void ThrowInvalidConversionSpecifier(LuaState state, ReadOnlySpan f ThrowInvalidConversionSpecifier(state, format); } - return builder.ToString(); + return builder.AsSpan().ToString(); } } diff --git a/src/Lua/Standard/OperatingSystemLibrary.cs b/src/Lua/Standard/OperatingSystemLibrary.cs index bfc38dfe..2bae9b86 100644 --- a/src/Lua/Standard/OperatingSystemLibrary.cs +++ b/src/Lua/Standard/OperatingSystemLibrary.cs @@ -219,11 +219,13 @@ CancellationToken cancellationToken { var table = context.GetArgument(0); var date = DateTimeHelper.ParseTimeTable(context.State, table); + var zone = context.State.Platform.TimeProvider.LocalTimeZone; return new( context.Return( DateTimeHelper.GetUnixTimeFromLocalTime( date, - context.State.Platform.TimeProvider.LocalTimeZone + zone, + zone.SupportsDaylightSavingTime && table["isdst"].ToBoolean() ) ) ); diff --git a/tests/Lua.Tests/DateBufferTests.cs b/tests/Lua.Tests/DateBufferTests.cs new file mode 100644 index 00000000..e4bec912 --- /dev/null +++ b/tests/Lua.Tests/DateBufferTests.cs @@ -0,0 +1,66 @@ +using System.Diagnostics.Tracing; +using Lua.Platforms; +using Lua.Standard; + +namespace Lua.Tests; + +[TestFixture(LuaStringMode.Utf16)] +[TestFixture(LuaStringMode.Bytes)] +public class DateBufferTests(LuaStringMode mode) +{ + [TestCase("%Q")] + [TestCase("%")] + [TestCase("%E")] + [NonParallelizable] + public void InvalidFormatsReturnEveryRentedBuffer(string suffix) + { + using var state = LuaState.Create( + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default + ); + state.OpenStandardLibraries(); + state.Environment["format"] = new string('x', 4096) + suffix; + var closure = state.Load("assert(not pcall(os.date,format))", "@date-buffer.lua"); + using var events = new PoolEvents(); + // Exclude one-time runtime initialization from buffer ownership tracking. + state.ExecuteAsync(closure, Memory.Empty).GetAwaiter().GetResult(); + events.ThreadId = Environment.CurrentManagedThreadId; + try + { + state.ExecuteAsync(closure, Memory.Empty).GetAwaiter().GetResult(); + } + finally + { + events.ThreadId = 0; + } + Assert.That(events.Rents, Is.GreaterThan(0)); + Assert.That(events.Outstanding, Is.Empty); + } + + sealed class PoolEvents : EventListener + { + public int ThreadId; + public int Rents; + public readonly HashSet<(int Buffer, int Pool)> Outstanding = new(); + + protected override void OnEventSourceCreated(EventSource source) + { + if (source.Name == "System.Buffers.ArrayPoolEventSource") + EnableEvents(source, EventLevel.Verbose); + } + + protected override void OnEventWritten(EventWrittenEventArgs e) + { + if (ThreadId == 0 || ThreadId != Environment.CurrentManagedThreadId) + return; + if (e.EventName == "BufferRented") + { + Rents++; + Outstanding.Add(((int)e.Payload![0]!, (int)e.Payload[2]!)); + } + else if (e.EventName == "BufferReturned") + { + Outstanding.Remove(((int)e.Payload![0]!, (int)e.Payload[2]!)); + } + } + } +} diff --git a/tests/Lua.Tests/DateFormatTests.cs b/tests/Lua.Tests/DateFormatTests.cs index 11a51620..04c3a555 100644 --- a/tests/Lua.Tests/DateFormatTests.cs +++ b/tests/Lua.Tests/DateFormatTests.cs @@ -73,31 +73,7 @@ public async Task LocalAndUtcDatesUseTheConfiguredZoneForCurrentAndExplicitTimes int hour ) { - var rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule( - new DateTime(2000, 1, 1), - new DateTime(2099, 12, 31), - TimeSpan.FromHours(1), - TimeZoneInfo.TransitionTime.CreateFloatingDateRule( - new DateTime(1, 1, 1, 2, 0, 0), - 3, - 2, - DayOfWeek.Sunday - ), - TimeZoneInfo.TransitionTime.CreateFloatingDateRule( - new DateTime(1, 1, 1, 2, 0, 0), - 11, - 1, - DayOfWeek.Sunday - ) - ); - var zone = TimeZoneInfo.CreateCustomTimeZone( - "Test/Pacific", - TimeSpan.FromHours(-8), - "Test Pacific", - "Test Standard", - "Test Daylight", - [rule] - ); + var zone = CreatePacificZone(TimeSpan.FromHours(1)); var provider = new FakeTimeProvider( new DateTimeOffset(2020, month, 15, 12, 0, 0, TimeSpan.Zero) ); @@ -135,6 +111,87 @@ int hour ); } + static TimeZoneInfo CreatePacificZone(TimeSpan daylightDelta) + { + var rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule( + new DateTime(2000, 1, 1), + new DateTime(2099, 12, 31), + daylightDelta, + TimeZoneInfo.TransitionTime.CreateFloatingDateRule( + new DateTime(1, 1, 1, 2, 0, 0), + 3, + 2, + DayOfWeek.Sunday + ), + TimeZoneInfo.TransitionTime.CreateFloatingDateRule( + new DateTime(1, 1, 1, 2, 0, 0), + 11, + 1, + DayOfWeek.Sunday + ) + ); + return TimeZoneInfo.CreateCustomTimeZone( + "Test/Pacific", + TimeSpan.FromHours(-8), + "Test Pacific", + "Test Standard", + "Test Daylight", + [rule] + ); + } + + [TestCase(1, 11, 1, 8)] + [TestCase(-1, 3, 8, 9)] + public async Task RepeatedLocalTimesRoundTripWithTheirDaylightFlag( + int delta, + int month, + int day, + int hour + ) + { + var zone = CreatePacificZone(TimeSpan.FromHours(delta)); + var first = new DateTimeOffset(2020, month, day, hour, 30, 0, TimeSpan.Zero); + var provider = new FakeTimeProvider(first); + provider.SetLocalTimeZone(zone); + using var state = CreateState(provider); + state.Environment["first"] = first.ToUnixTimeSeconds(); + await state.DoStringAsync( + """ + local a=os.date('*t',first) + local b=os.date('*t',first+3600) + local standard=b.isdst and first or first+3600 + assert(a.hour==b.hour and a.min==b.min and a.isdst~=b.isdst) + assert(os.time(a)==first and os.time(b)==first+3600) + assert(os.time(os.date('*t'))==first) + a.isdst=b.isdst + assert(os.time(a)==first+3600) + a.isdst=a.isdst and 0 or false + assert(os.time(a)==first+3600) + a.isdst=nil + assert(os.time(a)==standard) + """ + ); + } + + [TestCase(-1.9, "1969-12-31 23:59:59", 59)] + [TestCase(-0.5, "1970-01-01 00:00:00", 0)] + [TestCase(0.9999999, "1970-01-01 00:00:00", 0)] + [TestCase(1.9, "1970-01-01 00:00:01", 1)] + public async Task ExplicitTimestampsTruncateTowardZero( + double timestamp, + string expected, + int second + ) + { + using var state = CreateState(new FakeTimeProvider()); + state.Environment["timestamp"] = timestamp; + var result = await state.DoStringAsync( + "return os.date('!%Y-%m-%d %H:%M:%S',timestamp),os.date('!*t',timestamp).sec" + ); + Assert.That(result[0], Is.EqualTo(new LuaValue(expected))); + Assert.That(result[1].Read(), Is.EqualTo(second)); + } + [Test] public async Task NumericOffsetsIncludeMinutesWithoutAColon() { diff --git a/tests/Lua.Tests/DebugTracebackTests.cs b/tests/Lua.Tests/DebugTracebackTests.cs index c7ac72a3..48da4fc2 100644 --- a/tests/Lua.Tests/DebugTracebackTests.cs +++ b/tests/Lua.Tests/DebugTracebackTests.cs @@ -1,4 +1,5 @@ using Lua.Platforms; +using Lua.Runtime; using Lua.Standard; namespace Lua.Tests; @@ -7,6 +8,107 @@ namespace Lua.Tests; [TestFixture(LuaStringMode.Bytes)] public class DebugTracebackTests(LuaStringMode mode) { + [TestCase(20, 1, 22, 0)] + [TestCase(21, 1, 22, 11)] + [TestCase(100, 0, 23, 12)] + [TestCase(100, 1, 22, 11)] + [TestCase(100, 10, 13, 2)] + [TestCase(100, 11, 12, 1)] + [TestCase(100, 12, 91, 0)] + [TestCase(100, 100, 3, 0)] + [TestCase(100, 103, 0, 0)] + public async Task DeepTracebacksUseLua52AbbreviationAndSkipLevels( + int depth, + int level, + int expectedCount, + int expectedMarker + ) + { + using var state = LuaState.Create( + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default + ); + state.OpenStandardLibraries(); + state.Environment["depth"] = depth; + state.Environment["level"] = level; + var result = await state.DoStringAsync( + """ + local function recurse(n) + if n==0 then local trace=debug.traceback('message',level);return trace end + local trace=recurse(n-1) + return trace + end + local trace=recurse(depth) + assert(trace:sub(1,25)=='message\nstack traceback:\n' or trace=='message\nstack traceback:') + local count,marker=0,0 + for line in trace:gmatch('[^\n]+') do + if line:sub(1,1)=='\t' then + count=count+1 + if line=='\t...' then marker=count end + end + end + if count>0 then assert(trace:sub(-10)=='main chunk') end + return count,marker + """, + "@deep-traceback.lua" + ); + Assert.That(result, Is.EqualTo(new LuaValue[] { expectedCount, expectedMarker })); + } + + [TestCase(false)] + [TestCase(true)] + public async Task DeepCoroutineTracebacksAbbreviateSuspendedAndFailedStacks(bool fail) + { + using var state = LuaState.Create( + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default + ); + state.OpenStandardLibraries(); + state.Environment["fail"] = fail; + await state.DoStringAsync( + """ + local function recurse(n) + if n==0 then + if fail then error('failure',0) else coroutine.yield() end + end + local result=recurse(n-1) + return result + end + local co=coroutine.create(function() local result=recurse(100);return result end) + assert(coroutine.resume(co)==not fail) + local trace=debug.traceback(co,'message') + assert(trace:sub(1,25)=='message\nstack traceback:\n') + local count=0 + for line in trace:gmatch('[^\n]+') do if line:sub(1,1)=='\t' then count=count+1 end end + assert(count==23 and trace:find('\n\t...\n',1,true)) + """ + ); + } + + [Test] + public async Task HostTracebacksRetainAllFrames() + { + using var state = LuaState.Create( + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default + ); + state.OpenStandardLibraries(); + state.Environment["capture"] = new LuaFunction( + (context, _) => + new(context.Return(Traceback.CreateTracebackMessage(context.State, LuaValue.Nil))) + ); + await state.DoStringAsync( + """ + local function recurse(n) + if n==0 then local trace=capture();return trace end + local trace=recurse(n-1) + return trace + end + local trace=recurse(100) + local count=0 + for line in trace:gmatch('[^\n]+') do if line:sub(1,1)=='\t' then count=count+1 end end + assert(count>100 and not trace:find('\n\t...\n',1,true)) + """ + ); + } + [TestCase(LuaNumberFormat.DotNet)] [TestCase(LuaNumberFormat.Lua)] public async Task TracebackHandlesMessagesAndNegativeLevelsBeforeFormatting( From d9ea22c07ae7021f1d3a47fe7d576b0693c88e00 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:59:31 +0900 Subject: [PATCH 5/7] fix: close standard library API compatibility gaps Return the normal I/O error tuple for temporary-file creation and remove permission failures, while preserving cancellation. Accept numeric-string debug.getinfo levels and reuse the captured call-stack span. Keep stripped active-frame currentline at 0: native Lua 5.2 confirms this existing behavior is correct, so remove the incorrect documentation limitation. Convert difftime inputs to 64-bit whole seconds before double subtraction, including the omitted/nil zero base and positive zero results. Support fixed C-locale queries and validate the Lua 5.2 category names without changing host culture. Allow nonrecursive empty-directory removal on POSIX while preserving Windows C-runtime behavior and the existing file fast path. Add regressions for synchronous/asynchronous provider failures, cancellation, metadata coercion, fractional/default time differences, locale validation, and directory removal. Document Windows negative-timestamp restrictions. Leave collectgarbage unchanged. Validation: all 3,473 runtime tests pass on both net8.0 and net10.0; all 73 generator tests pass. All four library targets build without warnings. All 42 focused cases also pass on Ubuntu net10.0, including POSIX directory removal. Native Lua 5.2 comparisons pass for the shared cases; POSIX libc comparisons verify negative difftime inputs and directory behavior. Formatter and API-reference coverage/link checks pass; regressions fail on 655da93. Paired net10.0 benchmarks against 655da93 show numeric-level getinfo about 7% faster and function queries comparable, with unchanged allocations. Required difftime conversions add about 1-2 ns per call (3.5-4.2%) and remain allocation-free. --- docs/stdlib/debug.md | 4 +- docs/stdlib/io.md | 2 +- docs/stdlib/os.md | 6 +- src/Lua/IO/FileSystem.cs | 15 +++- src/Lua/Standard/DebugLibrary.cs | 17 ++-- src/Lua/Standard/IOLibrary.cs | 29 ++++-- src/Lua/Standard/OperatingSystemLibrary.cs | 21 ++--- tests/Lua.Tests/DebugLocalMetadataTests.cs | 25 ++++++ .../NotImplementedExceptionFileSystemBase.cs | 2 +- tests/Lua.Tests/IOTests.cs | 45 ++++++++++ .../Lua.Tests/StandardLibraryContractTests.cs | 69 +++++++++++++++ tests/Lua.Tests/TmpFileTests.cs | 88 +++++++++++++++++++ 12 files changed, 280 insertions(+), 43 deletions(-) create mode 100644 tests/Lua.Tests/TmpFileTests.cs diff --git a/docs/stdlib/debug.md b/docs/stdlib/debug.md index ef9dd454..4a59ea7a 100644 --- a/docs/stdlib/debug.md +++ b/docs/stdlib/debug.md @@ -8,7 +8,7 @@ Install with `state.OpenDebugLibrary()`. Thread-aware functions accept an option | --- | --- | --- | | [`debug.debug()`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.debug) | Officially enters an interactive debug command loop. | **Unavailable:** not registered. Interactive console/debugger integration must be provided by the host. | | [`debug.gethook([thread])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.gethook) | Returns the installed hook, mask, and count. After removing the hook: nil, empty mask, and zero count. | A host-installed hook is returned as its managed `LuaFunction`; C Lua reports a hook installed through its C API as the string `"external hook"`. The host hook's execution policy can differ from a Lua-installed hook; see below. | -| [`debug.getinfo([thread,] f [, what])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getinfo) | Returns function/frame metadata selected by `n`, `S`, `l`, `u`, `t`, `f`, and `L`, or nil for a missing stack level. | C# functions report `what="C#"` and `source="=[C#]"` instead of C-function labels. `u` exposes managed closure capture counts. **Limitation:** numeric strings are not accepted as stack levels, because the function/level dispatch tests the Lua value tag before numeric coercion. | +| [`debug.getinfo([thread,] f [, what])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getinfo) | Returns function/frame metadata selected by `n`, `S`, `l`, `u`, `t`, `f`, and `L`, or nil for a missing stack level. Numeric-string levels are accepted and fractions truncate toward zero, as in Lua 5.2. | C# functions report `what="C#"` and `source="=[C#]"` instead of C-function labels. `u` exposes managed closure capture counts. | | [`debug.getlocal([thread,] f, index)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getlocal) | Inspects a frame local/temporary/vararg, or a Lua function's parameter name; returns nil when no such local exists. | Managed frames expose their Lua-facing argument/temporary slots, not arbitrary C# local variables. The implementation operates on Lua-CSharp stack metadata. | | [`debug.getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getmetatable) | Returns the actual metatable, bypassing `__metatable` protection. | May return a host-frozen generated metatable. Reading the actual table does not make its contents writable. | | [`debug.getregistry()`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getregistry) | Returns the state's registry table. | Registry contents are managed runtime state; C API integer-slot conventions and native pointers are not a portable contract for Lua-CSharp. | @@ -29,7 +29,7 @@ Hook callbacks receive an event string and a line number or nil. Events include Hooks installed from C# through `LuaState.SetHook` retain the host hook policy, including supported async/yield behavior; they are distinguished internally from `debug.sethook` callbacks. This lets host instrumentation integrate with async execution while keeping Lua-installed callbacks compatible with non-yieldable Lua 5.2 hooks. See the [C# debug-hook guide](../../README.md#debug-hooks). -Stripped bytecode has no line/local names to recover. Source/line fields and local lookup reflect the metadata actually present; dumping and reloading a stripped chunk cannot reconstruct it. **Limitation:** `getinfo` can report an unknown Lua line as `0` instead of C Lua's `-1`, because the managed line lookup uses zero when line info is absent. +Stripped bytecode has no line/local names to recover. Source/line fields and local lookup reflect the metadata actually present; dumping and reloading a stripped chunk cannot reconstruct it. As in [C Lua 5.2](https://www.lua.org/source/5.2/ldebug.c.html#currentline), `getinfo` reports `0` for an active stripped Lua frame, and `-1` for a function-only query or a C# frame. ## Traceback details diff --git a/docs/stdlib/io.md b/docs/stdlib/io.md index 401d9d95..618ed9eb 100644 --- a/docs/stdlib/io.md +++ b/docs/stdlib/io.md @@ -19,7 +19,7 @@ Install with `state.OpenIOLibrary()`. Operations use `LuaPlatform.FileSystem` an | [`io.stderr`](https://www.lua.org/manual/5.2/manual.html#pdf-io.stderr) | Initial standard error handle. | **Host-dependent:** wraps the configured error stream; default console handles cannot be closed through Lua. | | [`io.stdin`](https://www.lua.org/manual/5.2/manual.html#pdf-io.stdin) | Initial standard input handle. | **Host-dependent:** wraps the configured input stream. Selecting another `io.input` does not replace this field. | | [`io.stdout`](https://www.lua.org/manual/5.2/manual.html#pdf-io.stdout) | Initial standard output handle. | **Host-dependent:** wraps the configured output stream. Selecting another `io.output` does not replace this field. | -| [`io.tmpfile()`](https://www.lua.org/manual/5.2/manual.html#pdf-io.tmpfile) | Creates a temporary read/write file handle. | The default provider uses a .NET `FileStream` with `DeleteOnClose`. **Limitation:** creation failures are raised rather than converted to Lua's usual nil/error result; this entry directly awaits the provider. Close explicitly for deterministic deletion. | +| [`io.tmpfile()`](https://www.lua.org/manual/5.2/manual.html#pdf-io.tmpfile) | Creates a temporary read/write file handle, or returns `nil, message, code` for handled I/O/permission failures. | The default provider uses a .NET `FileStream` with `DeleteOnClose`. Error messages/codes come from the managed provider; cancellation still propagates to the host. Close explicitly for deterministic deletion. | | [`io.type(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-io.type) | Returns `file`, `closed file`, or nil. | Recognizes managed `FileHandle` objects and queries their provider's open state. | | [`io.write(...)`](https://www.lua.org/manual/5.2/manual.html#pdf-io.write) | Writes string/number arguments to default output and returns that file on success. | Numbers use `NumberFormat`; strings use the configured stream encoding. | diff --git a/docs/stdlib/os.md b/docs/stdlib/os.md index 8c6f5fcc..c65871b7 100644 --- a/docs/stdlib/os.md +++ b/docs/stdlib/os.md @@ -8,13 +8,13 @@ Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process ex | --- | --- | --- | | [`os.clock()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.clock) | Returns elapsed CPU time in seconds. | **Host-dependent:** calls `ILuaOsEnvironment.GetTotalProcessorTime`. The default reads the current .NET process's total processor time, including host work outside Lua. | | [`os.date([format [, time]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.date) | Formats a timestamp, or returns date fields for `*t`; a leading `!` selects UTC. Defaults to current time and `%c`. Explicit timestamps are truncated toward zero to whole Unix seconds, as in C Lua 5.2. | Uses managed date/time formatting and the configured `TimeProvider.LocalTimeZone` rather than C `strftime`/process timezone state. Names and composite formats use the C-locale conventions below. | -| [`os.difftime(t2, t1)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns `t2 - t1` in seconds. | Direct double subtraction preserves fractions instead of converting to C `time_t` first. **Limitation:** both arguments are required; C Lua 5.2.4 also accepts an omitted/nil second argument as zero, although the manual lists both arguments. | +| [`os.difftime(t2 [, t1])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.difftime) | Returns the difference in whole Unix seconds. Both timestamps truncate toward zero before subtraction; omitted/nil `t1` defaults to zero, as in C Lua 5.2.4's implementation. | Converts to managed 64-bit seconds and subtracts as doubles, without integer subtraction overflow. C `time_t` ranges vary by build; unlike the Windows C runtime, managed arithmetic supports negative timestamps. The official manual lists both arguments, but the C implementation also accepts the zero default. | | [`os.execute([command])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.execute) | Officially runs a shell command or queries shell availability. | **Unsupported operation:** no command/nil returns false; supplying a command raises a not-supported error. The library has no shell-execution implementation. | | [`os.exit([code [, close]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.exit) | Requests termination with a numeric code or boolean success/failure code. Numeric strings are coerced and fractions truncated toward zero to a managed 32-bit code. | **Host-dependent:** default `SystemOsEnvironment` calls `Environment.Exit`; Unity uses its application exit provider. **Limitation:** `close` is ignored, so the Lua state is not explicitly disposed before exiting. | | [`os.getenv(name)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.getenv) | Returns an environment value or nil. | **Host-dependent:** default provider reads process environment variables; custom/Unity providers may expose a managed dictionary instead. Names/results are converted between byte strings and .NET text when needed. | -| [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a path or reports failure. | **Limitation:** default `FileSystem.Remove` removes files only, even on POSIX hosts where C Lua can remove empty directories. It uses `File.Exists`/`File.Delete` and has no directory-removal path. Handled errors use .NET HResult codes. | +| [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a file or, on POSIX hosts, an empty directory. Returns true on success or `nil, message, code` for handled I/O/permission failures. | The default provider uses .NET filesystem operations and never removes directories recursively. Windows rejects directory removal, matching its C runtime. Handled errors use .NET HResult codes. | | [`os.rename(oldname, newname)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.rename) | Moves/renames a file or directory; reports success or handled failure. | **Host-dependent:** default provider uses `File.Move`/`File.Replace` and `Directory.Move`. Directory replacement restrictions follow .NET/host filesystem behavior rather than C `rename`; .NET Standard 2.1 uses `File.Replace` for an existing file destination. | -| [`os.setlocale(locale [, category])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Officially selects or queries a C locale. | **Limitation:** only the literal `C` is accepted; other strings return nil, nil is not a locale-query operation, and `category` is ignored. No C or .NET locale is changed. Date formatting stays invariant; string casing/classification follows the [string library's rules](string.md). | +| [`os.setlocale([locale [, category]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Queries the fixed locale with omitted/nil input, or accepts `C`; both return `C`. Validates `all`, `collate`, `ctype`, `monetary`, `numeric`, and `time` categories; the default is `all`. | **Limitation:** other locale strings return nil; no C or .NET locale is changed. Date formatting stays invariant; string casing/classification follows the [string library's rules](string.md). | | [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. `isdst` disambiguates repeated local times; see below. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. Current time and the local timezone come from `TimeProvider`, consistently with `os.date`. | | [`os.tmpname()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.tmpname) | Returns a temporary pathname. | **Host-dependent:** default `Path.GetTempFileName` creates an empty file while choosing the name. The caller owns cleanup; unlike `io.tmpfile`, this API does not attach delete-on-close behavior. The name uses the state's string representation. | diff --git a/src/Lua/IO/FileSystem.cs b/src/Lua/IO/FileSystem.cs index f9d57a4e..1ac6a27c 100644 --- a/src/Lua/IO/FileSystem.cs +++ b/src/Lua/IO/FileSystem.cs @@ -1,4 +1,6 @@ -namespace Lua.IO; +using System.Runtime.InteropServices; + +namespace Lua.IO; /// Directory that relative paths are resolved against. /// @@ -121,12 +123,19 @@ public ValueTask Rename(string oldName, string newName, CancellationToken cancel public ValueTask Remove(string path, CancellationToken cancellationToken) { path = GetFullPath(path); - if (!File.Exists(path)) + if (File.Exists(path)) + { + File.Delete(path); + } + else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Directory.Exists(path)) + { + Directory.Delete(path, recursive: false); + } + else { throw new FileNotFoundException("No such file or directory", path); } - File.Delete(path); return default; } diff --git a/src/Lua/Standard/DebugLibrary.cs b/src/Lua/Standard/DebugLibrary.cs index 85b68964..e1b92c06 100644 --- a/src/Lua/Standard/DebugLibrary.cs +++ b/src/Lua/Standard/DebugLibrary.cs @@ -554,7 +554,6 @@ public ValueTask GetInfo( CancellationToken cancellationToken ) { - // return new(0); var state = GetLuaThread(context, out var argOffset); var what = context.GetArgumentOrDefault(argOffset + 1, "flnStu"); CallStackFrame? previousFrame = null; @@ -562,12 +561,10 @@ CancellationToken cancellationToken var pc = 0; var arg1 = context.GetArgument(argOffset); - if (arg1.TryReadFunction(out var functionToInspect)) - { - // what = ">" + what; - } - else if (arg1.TryReadNumber(out _)) + if (!arg1.TryReadFunction(out var functionToInspect)) { + if (!arg1.TryReadDouble(out _)) + context.ThrowBadArgument(argOffset, "function or level expected"); var level = context.GetIntegerArgument(argOffset) + 1; var callStack = state.GetCallStackFrames(); @@ -577,19 +574,15 @@ CancellationToken cancellationToken return new(context.Return(LuaValue.Nil)); } - currentFrame = state.GetCallStackFrames()[^level]; + currentFrame = callStack[^level]; previousFrame = level + 1 <= callStack.Length ? callStack[^(level + 1)] : null; if (level != 1) { - pc = state.GetCallStackFrames()[^(level - 1)].CallerInstructionIndex; + pc = callStack[^(level - 1)].CallerInstructionIndex; } functionToInspect = currentFrame.Value.Function; } - else - { - context.ThrowBadArgument(argOffset, "function or level expected"); - } using var debug = LuaDebug.Create( context.GlobalState, diff --git a/src/Lua/Standard/IOLibrary.cs b/src/Lua/Standard/IOLibrary.cs index 29c6ae35..8444f20e 100644 --- a/src/Lua/Standard/IOLibrary.cs +++ b/src/Lua/Standard/IOLibrary.cs @@ -313,15 +313,26 @@ public async ValueTask TmpFile( CancellationToken cancellationToken ) { - return context.Return( - LuaValue.FromUserData( - new FileHandle( - await context.GlobalState.Platform.FileSystem.OpenTempFileStream( - cancellationToken - ), - context.GlobalState.FileHandleMetatable + try + { + return context.Return( + LuaValue.FromUserData( + new FileHandle( + await context.GlobalState.Platform.FileSystem.OpenTempFileStream( + cancellationToken + ), + context.GlobalState.FileHandleMetatable + ) ) - ) - ); + ); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return context.Return( + LuaValue.Nil, + context.State.Platform.EncodeText(ex.Message), + ex.HResult + ); + } } } diff --git a/src/Lua/Standard/OperatingSystemLibrary.cs b/src/Lua/Standard/OperatingSystemLibrary.cs index 2bae9b86..b900af79 100644 --- a/src/Lua/Standard/OperatingSystemLibrary.cs +++ b/src/Lua/Standard/OperatingSystemLibrary.cs @@ -86,9 +86,9 @@ public ValueTask DiffTime( CancellationToken cancellationToken ) { - var t2 = context.GetArgument(0); - var t1 = context.GetArgument(1); - return new(context.Return(t2 - t1)); + var t2 = (long)context.GetArgument(0); + var t1 = context.HasArgument(1) ? (long)context.GetArgument(1) : 0; + return new(context.Return((double)t2 - t1)); } public ValueTask Execute( @@ -158,7 +158,7 @@ await context.GlobalState.Platform.FileSystem.Remove( ); return context.Return(true); } - catch (IOException ex) + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return context.Return( LuaValue.Nil, @@ -199,15 +199,12 @@ public ValueTask SetLocale( CancellationToken cancellationToken ) { - // os.setlocale is not supported besides the standard "C" POSIX locale - // Any locale besides "C" will return nil - var locale = context.GetStringArgument(0); - if (locale == "C") - { - return new(context.Return(locale)); - } + var locale = context.HasArgument(0) ? context.GetStringArgument(0) : null; + var category = context.HasArgument(1) ? context.GetStringArgument(1) : "all"; + if (category is not ("all" or "collate" or "ctype" or "monetary" or "numeric" or "time")) + LuaRuntimeException.BadArgument(context.State, 2, $"invalid option '{category}'"); - return new(context.Return(LuaValue.Nil)); + return new(context.Return(locale is null or "C" ? "C" : LuaValue.Nil)); } public ValueTask Time( diff --git a/tests/Lua.Tests/DebugLocalMetadataTests.cs b/tests/Lua.Tests/DebugLocalMetadataTests.cs index 297df40e..14799e8f 100644 --- a/tests/Lua.Tests/DebugLocalMetadataTests.cs +++ b/tests/Lua.Tests/DebugLocalMetadataTests.cs @@ -8,6 +8,31 @@ namespace Lua.Tests; [TestFixture(LuaStringMode.Bytes)] public class DebugLocalMetadataTests(LuaStringMode mode) { + [TestCase("1", false)] + [TestCase(" 1.9 ", false)] + [TestCase("0x1", false)] + [TestCase("-1", false)] + [TestCase("1", true)] + public async Task GetInfoCoercesNumericStringLevels(string index, bool explicitThread) + { + using var state = LuaState.Create( + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default + ); + state.OpenStandardLibraries(); + state.Environment["index"] = index; + state.Environment["thread"] = state; + var prefix = explicitThread ? "thread," : ""; + await state.DoStringAsync( + $$""" + local a,b=debug.getinfo({{prefix}}index,'flS'),debug.getinfo({{prefix}}tonumber(index),'flS') + if a==nil then assert(b==nil) else + assert(a.func==b.func and a.currentline==b.currentline and a.source==b.source) + end + assert(not pcall(debug.getinfo,'not a level')) + """ + ); + } + [TestCase(false)] [TestCase(true)] public async Task WrappedCoroutineCanInspectItsParentsNativeFrame(bool asynchronous) diff --git a/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs b/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs index f3835b7b..107d55fb 100644 --- a/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs +++ b/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs @@ -39,7 +39,7 @@ public virtual string GetTempFileName() throw new NotImplementedException(); } - public ValueTask OpenTempFileStream(CancellationToken cancellationToken) + public virtual ValueTask OpenTempFileStream(CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/tests/Lua.Tests/IOTests.cs b/tests/Lua.Tests/IOTests.cs index 55816530..cf395389 100644 --- a/tests/Lua.Tests/IOTests.cs +++ b/tests/Lua.Tests/IOTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using System.Text; using Lua.IO; using Lua.Standard; @@ -443,6 +444,50 @@ public async Task FileSystem_Remove_Works() Assert.That(File.Exists(testFile), Is.False); } + [Test] + public async Task OsRemoveHandlesEmptyDirectoriesAccordingToTheHost() + { + var path = GetTestFilePath("empty"); + Directory.CreateDirectory(path); + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + state.Environment["path"] = path; + var result = await state.DoStringAsync("return os.remove(path)"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.That(result, Has.Length.EqualTo(3)); + Assert.That(result[0], Is.EqualTo(LuaValue.Nil)); + Assert.That(Directory.Exists(path), Is.True); + } + else + { + Assert.That(result, Is.EqualTo(new LuaValue[] { true })); + Assert.That(Directory.Exists(path), Is.False); + } + } + + [Test] + public async Task OsRemovePreservesNonemptyDirectoriesAndReportsMissingPaths() + { + var path = GetTestFilePath("nonempty"); + Directory.CreateDirectory(path); + var child = Path.Combine(path, "child.txt"); + File.WriteAllText(child, "keep"); + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + state.Environment["path"] = path; + state.Environment["missing"] = GetTestFilePath("missing"); + await state.DoStringAsync( + """ + for _,name in ipairs({path,missing}) do + local values=table.pack(os.remove(name)) + assert(values.n==3 and values[1]==nil and type(values[2])=='string' and type(values[3])=='number') + end + """ + ); + Assert.That(File.ReadAllText(child), Is.EqualTo("keep")); + } + [Test] public void FileSystem_IsReadable_Works() { diff --git a/tests/Lua.Tests/StandardLibraryContractTests.cs b/tests/Lua.Tests/StandardLibraryContractTests.cs index 09280f67..59a8f347 100644 --- a/tests/Lua.Tests/StandardLibraryContractTests.cs +++ b/tests/Lua.Tests/StandardLibraryContractTests.cs @@ -18,6 +18,75 @@ LuaState CreateState() return state; } + [TestCase("2.8,1.2", 1)] + [TestCase("-2.8,-1.2", -1)] + [TestCase("-0.5,0.5", 0)] + [TestCase("'2.8','-1.9'", 3)] + [TestCase("2.8", 2)] + [TestCase("2.8,nil", 2)] + public async Task DiffTimeUsesWholeSecondsAndDefaultsItsBaseToZero( + string arguments, + double expected + ) + { + using var state = CreateState(); + var result = await state.DoStringAsync($"return os.difftime({arguments})"); + Assert.That(result, Is.EqualTo(new LuaValue[] { expected })); + if (expected == 0) + Assert.That(1 / result[0].Read(), Is.EqualTo(double.PositiveInfinity)); + } + + [Test] + public async Task SetLocaleQueriesAndValidatesTheFixedCLocale() + { + using var state = CreateState(); + var culture = System.Globalization.CultureInfo.CurrentCulture; + await state.DoStringAsync( + """ + assert(os.setlocale()=='C' and os.setlocale(nil)=='C') + assert(os.setlocale('C',nil)=='C') + for _,category in ipairs({'all','collate','ctype','monetary','numeric','time'}) do + assert(os.setlocale(nil,category)=='C') + assert(os.setlocale('C',category)=='C') + assert(os.setlocale('unsupported locale',category)==nil) + assert(os.setlocale(nil,category)=='C') + end + local ok,e=pcall(os.setlocale,nil,'invalid') + assert(not ok and e:find('invalid option',1,true)) + assert(not pcall(os.setlocale,'unsupported locale','invalid')) + assert(not pcall(os.setlocale,false)) + """ + ); + Assert.That(System.Globalization.CultureInfo.CurrentCulture, Is.SameAs(culture)); + } + + [Test] + public async Task RemovePermissionFailuresReturnTheIoErrorTuple() + { + var error = new UnauthorizedAccessException("denied ế"); + using var state = CreateState(); + state.Platform = state.Platform with { FileSystem = new DeniedRemoveFileSystem(error) }; + var result = await state.DoStringAsync("return pcall(os.remove,'file')"); + Assert.That( + result, + Is.EqualTo( + new LuaValue[] + { + true, + LuaValue.Nil, + state.Platform.EncodeText(error.Message), + error.HResult, + } + ) + ); + } + + sealed class DeniedRemoveFileSystem(Exception error) : NotImplementedExceptionFileSystemBase + { + public override ValueTask Remove(string path, CancellationToken cancellationToken) => + throw error; + } + [Test] public async Task RawSetReturnsTheOriginalTable() { diff --git a/tests/Lua.Tests/TmpFileTests.cs b/tests/Lua.Tests/TmpFileTests.cs new file mode 100644 index 00000000..86f4f1c9 --- /dev/null +++ b/tests/Lua.Tests/TmpFileTests.cs @@ -0,0 +1,88 @@ +using Lua.IO; +using Lua.Platforms; +using Lua.Standard; +using Lua.Tests.Helpers; + +namespace Lua.Tests; + +[TestFixture(LuaStringMode.Utf16)] +[TestFixture(LuaStringMode.Bytes)] +public class TmpFileTests(LuaStringMode mode) +{ + LuaState CreateState(ILuaFileSystem? fileSystem = null) + { + var platform = + mode == LuaStringMode.Bytes ? LuaPlatform.DefaultByteStrings : LuaPlatform.Default; + if (fileSystem is not null) + platform = platform with { FileSystem = fileSystem }; + var state = LuaState.Create(platform); + state.OpenStandardLibraries(); + return state; + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public async Task CreationFailuresReturnTheIoErrorTuple(bool denied, bool asynchronous) + { + Exception error = denied + ? new UnauthorizedAccessException("denied ế") + : new IOException("failed ế"); + using var state = CreateState(new FailingFileSystem(error, asynchronous)); + var result = await state.DoStringAsync("return pcall(io.tmpfile)"); + Assert.That( + result, + Is.EqualTo( + new LuaValue[] + { + true, + LuaValue.Nil, + state.Platform.EncodeText(error.Message), + error.HResult, + } + ) + ); + } + + [TestCase(false)] + [TestCase(true)] + public void CreationCancellationStillEscapesPcall(bool asynchronous) + { + using var state = CreateState( + new FailingFileSystem(new OperationCanceledException(), asynchronous) + ); + Assert.ThrowsAsync(async () => + await state.DoStringAsync("return pcall(io.tmpfile)") + ); + } + + [Test] + public async Task SuccessfulCreationReturnsOneUsableFile() + { + using var state = CreateState(); + await state.DoStringAsync( + """ + local values=table.pack(io.tmpfile()) + assert(values.n==1 and io.type(values[1])=='file') + local file=values[1] + assert(file:write('temporary')==file) + assert(file:seek('set',0)==0 and file:read('*a')=='temporary') + assert(file:close() and io.type(file)=='closed file') + """ + ); + } + + sealed class FailingFileSystem(Exception error, bool asynchronous) + : NotImplementedExceptionFileSystemBase + { + public override async ValueTask OpenTempFileStream( + CancellationToken cancellationToken + ) + { + if (asynchronous) + await Task.Yield(); + throw error; + } + } +} From bb99a293c18b643a4f63cede047face24a8de29b Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:08:40 +0900 Subject: [PATCH 6/7] fix: align date tables, light userdata and stdin loading with Lua 5.2 Normalize os.time calendar fields, accept Lua numeric coercion and optional field defaults, and read metatable-backed fields in Lua 5.2 order. Keep ordinary tables synchronous and preserve the non-yieldable Lua boundary. Return nil when the managed date range cannot represent a conversion. Store the light-userdata type metatable in the shared global state. Load omitted/nil filenames from host standard input through the file loader, preserving raw bytecode, chunk names, environments and stream ownership. Add 29 regression cases and update per-API documentation, including the remaining managed date range and DST differences. Leave collectgarbage unchanged. Validation: 3,502 runtime tests per net8.0/net10.0, 73 generator tests, 29 focused Linux cases, all library target builds and formatting/docs checks. Compared behavior with native Lua 5.2, including redirected standard input. Paired .NET 10 benchmarks keep allocations unchanged: full date tables are flat, defaulted date tables add about 7 ns/call, and small-file loading and userdata lookup stay close to baseline. --- docs/stdlib/basic.md | 4 +- docs/stdlib/debug.md | 2 +- docs/stdlib/os.md | 4 +- src/Lua/IO/StandardIOStream.cs | 2 + src/Lua/Internal/LuaGlobalState.cs | 5 + src/Lua/LuaStateExtensions.cs | 48 +++++- src/Lua/Standard/BasicLibrary.cs | 8 +- src/Lua/Standard/Internal/DateTimeHelper.cs | 158 ++++++++++++++---- src/Lua/Standard/OperatingSystemLibrary.cs | 36 ++-- .../Lua.Tests/LightUserDataMetatableTests.cs | 60 +++++++ tests/Lua.Tests/StandardInputLoadingTests.cs | 151 +++++++++++++++++ tests/Lua.Tests/TimeTableTests.cs | 133 +++++++++++++++ 12 files changed, 556 insertions(+), 55 deletions(-) create mode 100644 tests/Lua.Tests/LightUserDataMetatableTests.cs create mode 100644 tests/Lua.Tests/StandardInputLoadingTests.cs create mode 100644 tests/Lua.Tests/TimeTableTests.cs diff --git a/docs/stdlib/basic.md b/docs/stdlib/basic.md index 93d02928..e40c25e2 100644 --- a/docs/stdlib/basic.md +++ b/docs/stdlib/basic.md @@ -8,13 +8,13 @@ Install with `state.OpenBasicLibrary()`. Functions are globals. The [shared stri | --- | --- | --- | | [`assert(v [, message])`](https://www.lua.org/manual/5.2/manual.html#pdf-assert) | Returns all arguments when `v` is truthy; otherwise raises an assertion error. The default message is `assertion failed!`. | The host receives a `LuaAssertionException`. Lua 5.2's message is string-like: a number is converted to text; an arbitrary table is not an assertion error object. | | [`collectgarbage([opt [, arg]])`](https://www.lua.org/manual/5.2/manual.html#pdf-collectgarbage) | Recognizes `collect`, `stop`, `restart`, `count`, `step`, `setpause`, `setstepmul`, `setmajorinc`, `isrunning`, `incremental`, and `generational`. | **Limitation:** every recognized option calls `GC.Collect()` and returns **no values**. `arg` is ignored. Thus `count` does not report memory, `stop` does not stop GC, and tuning/mode options do not tune it. The implementation delegates to .NET GC without implementing Lua's collector control protocol. | -| [`dofile([filename])`](https://www.lua.org/manual/5.2/manual.html#pdf-dofile) | Loads and executes a file; returns its results and raises load/runtime failures. | **Limitation:** a filename is required; omitted/nil input does not load standard input. Loading goes through `LuaPlatform.FileSystem`. Paths are decoded to .NET text in byte mode. | +| [`dofile([filename])`](https://www.lua.org/manual/5.2/manual.html#pdf-dofile) | Loads and executes a file, or standard input for an omitted/nil filename; returns its results and raises load/runtime failures. | Files use `LuaPlatform.FileSystem`; standard input uses `LuaPlatform.StandardIO.Input` and remains open. Paths are decoded to .NET text in byte mode. | | [`error(message [, level])`](https://www.lua.org/manual/5.2/manual.html#pdf-error) | Raises the supplied error object. At positive levels, numbers become strings and string messages receive a source prefix when that level exists. At level 0 or below, the original object is preserved. | Host-facing errors use `LuaRuntimeException`; stack descriptions include managed frames. Numeric text follows `NumberFormat`. | | [`_G`](https://www.lua.org/manual/5.2/manual.html#pdf-_G) | The initial global environment table. | Installed from the state's managed environment. Assigning `_G` does not rewrite a function's `_ENV`. | | [`getmetatable(value)`](https://www.lua.org/manual/5.2/manual.html#pdf-getmetatable) | Returns the metatable, its protected `__metatable` value, or nil. | Also works with managed userdata. | | [`ipairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-ipairs) | Uses `__ipairs` when present; otherwise returns an iterator, the table, and `0`, stopping at the first missing integer element. The default iterator converts its control value to an integer and returns one nil at exhaustion, as in Lua 5.2. | `__ipairs` cannot yield across the library callback boundary. | | [`load(ld [, source [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-load) | Compiles source/bytecode from a string or a reader function. Returns a function, or `nil, error`; an empty reader result ends input. `mode` defaults to `bt`. Explicit `env`, including nil, initializes the first upvalue. | **Limitation:** reader pieces are collected before parsing, so readers can be called past the point where C Lua would detect a syntax error or finish a binary chunk. This follows the current whole-input compiler interface. Reader callbacks cannot Lua-yield, but C# readers can await. Raw binary strings work in either mode; see [dumping](string.md#dumping-and-loading). | -| [`loadfile([filename [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-loadfile) | Compiles a file without running it; returns a function or `nil, error`. Recognizes an initial UTF-8 BOM and first-line `#` comment. | **Limitation:** filename is required; there is no standard-input overload in the Lua library. Uses the configured filesystem, with byte-mode paths decoded to .NET text. | +| [`loadfile([filename [, mode [, env]]])`](https://www.lua.org/manual/5.2/manual.html#pdf-loadfile) | Compiles a file, or standard input for an omitted/nil filename; returns a function or `nil, error`. Recognizes an initial UTF-8 BOM and first-line `#` comment. | Uses the configured filesystem, with byte-mode paths decoded to .NET text. Standard input uses `LuaPlatform.StandardIO.Input`, independently of `io.input`/`io.stdin` assignments, has chunk name `=stdin`, and remains open on success or failure. Mode/environment handling is the same for both sources; bytecode requires an input provider exposing raw bytes through `ILuaByteStream`, as the default console provider does. | | [`next(t [, index])`](https://www.lua.org/manual/5.2/manual.html#pdf-next) | Returns the next key/value, or nil after the last key. Omitted/nil `index` starts traversal. | Order follows the managed table storage and is unspecified. **Limitation:** an unknown hash key such as `next({}, 'missing')` returns nil instead of C Lua's invalid-key error: the managed lookup treats that missing iteration position as the end. | | [`pairs(t)`](https://www.lua.org/manual/5.2/manual.html#pdf-pairs) | Uses `__pairs`, or returns the default iterator, table, and nil. | `__pairs` cannot Lua-yield across the library callback boundary. | | [`pcall(f [, ...])`](https://www.lua.org/manual/5.2/manual.html#pdf-pcall) | Returns `true, ...results`, or `false, errorObject`. Callable values with `__call` are supported. | Host execution cancellation escapes to C# rather than being consumed as a script error. | diff --git a/docs/stdlib/debug.md b/docs/stdlib/debug.md index 4a59ea7a..9e4521ea 100644 --- a/docs/stdlib/debug.md +++ b/docs/stdlib/debug.md @@ -16,7 +16,7 @@ Install with `state.OpenDebugLibrary()`. Thread-aware functions accept an option | [`debug.getuservalue(u)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.getuservalue) | Reads a userdata's first user-value slot, or nil when absent. | Uses `ILuaUserData.UserValues`. Host userdata can expose no slots; values are `LuaValue` entries rather than Lua 5.2's table/nil-only uservalue model. | | [`debug.sethook([thread,] hook, mask [, count])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.sethook) | Installs/removes a hook; mask letters are `c`, `r`, and `l`, with an optional instruction count. | Lua-installed hooks cannot Lua-yield. They can call C# code that awaits. Counting is based on VM instructions, not time spent inside a C# function. See [hook details](#hook-details). | | [`debug.setlocal([thread,] level, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setlocal) | Writes a frame local and returns its name, or nil when unavailable. | Writes Lua-facing managed stack slots; it cannot set arbitrary CLR locals. | -| [`debug.setmetatable(value, mt)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setmetatable) | Replaces/removes the actual metatable, bypassing `__metatable` protection; returns the value. | Host-frozen tables still reject changing their metatable because `LuaTable.Freeze()` protects managed storage. **Limitation:** light-userdata object tokens have no case in the managed metatable dispatcher, so setting their metatable has no effect. | +| [`debug.setmetatable(value, mt)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setmetatable) | Replaces/removes the actual metatable, bypassing `__metatable` protection; returns the value. | Host-frozen tables still reject changing their metatable because `LuaTable.Freeze()` protects managed storage. Light userdata, including `debug.upvalueid` tokens and plain host-object values, share one type metatable within a global state and its coroutines, as in Lua 5.2; independent states have separate metatables. | | [`debug.setupvalue(f, index, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setupvalue) | Writes an upvalue and returns its name, or no values for an invalid slot. | Can update managed closure capture slots; their names are empty. Stripped Lua names are also empty, consistently with `getupvalue` and C Lua 5.2.4. | | [`debug.setuservalue(u, value)`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.setuservalue) | Writes the first user-value slot and returns the userdata. | **Difference/limitation:** any Lua value is accepted, rather than Lua 5.2's table/nil restriction. Userdata exposing no `UserValues` storage returns nil. This reflects the host-defined `Span` storage API. | | [`debug.traceback([thread,] [message [, level]])`](https://www.lua.org/manual/5.2/manual.html#pdf-debug.traceback) | Builds a traceback; non-string/non-number messages pass through unchanged without inspecting the level. | Formats managed/C# frames and Lua source metadata rather than reproducing C Lua's exact trace text. See [traceback details](#traceback-details). | diff --git a/docs/stdlib/os.md b/docs/stdlib/os.md index c65871b7..67fe2221 100644 --- a/docs/stdlib/os.md +++ b/docs/stdlib/os.md @@ -15,7 +15,7 @@ Install with `state.OpenOperatingSystemLibrary()`. Time, environment, process ex | [`os.remove(filename)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.remove) | Removes a file or, on POSIX hosts, an empty directory. Returns true on success or `nil, message, code` for handled I/O/permission failures. | The default provider uses .NET filesystem operations and never removes directories recursively. Windows rejects directory removal, matching its C runtime. Handled errors use .NET HResult codes. | | [`os.rename(oldname, newname)`](https://www.lua.org/manual/5.2/manual.html#pdf-os.rename) | Moves/renames a file or directory; reports success or handled failure. | **Host-dependent:** default provider uses `File.Move`/`File.Replace` and `Directory.Move`. Directory replacement restrictions follow .NET/host filesystem behavior rather than C `rename`; .NET Standard 2.1 uses `File.Replace` for an existing file destination. | | [`os.setlocale([locale [, category]])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.setlocale) | Queries the fixed locale with omitted/nil input, or accepts `C`; both return `C`. Validates `all`, `collate`, `ctype`, `monetary`, `numeric`, and `time` categories; the default is `all`. | **Limitation:** other locale strings return nil; no C or .NET locale is changed. Date formatting stays invariant; string casing/classification follows the [string library's rules](string.md). | -| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, converts local date fields; defaults are hour 12, minute/second 0. The input table remains unchanged, as in C Lua 5.2.4. `isdst` disambiguates repeated local times; see below. | **Limitation:** fields must be integral and fit the .NET `DateTime` constructor; fractional fields are rejected instead of truncated, and out-of-range dates are rejected instead of normalized by C `mktime`. Current time and the local timezone come from `TimeProvider`, consistently with `os.date`. | +| [`os.time([table])`](https://www.lua.org/manual/5.2/manual.html#pdf-os.time) | Without a table, returns current Unix seconds. With a table, reads local date fields through `__index`, accepts numeric strings, truncates fractions toward zero, and normalizes calendar overflow such as day 0 or month 13. Invalid/missing optional hour/minute/second fields default to 12/0/0; invalid/missing required year/month/day fields raise an error. The input table remains unchanged, as in C Lua 5.2.4. | Current time and the local timezone come from `TimeProvider`, consistently with `os.date`. Conversion returns nil for dates outside managed date/time ranges or timestamp -1, Lua 5.2's failure sentinel. `__index` callbacks cannot Lua-yield through this library boundary, but C# callbacks can await. See below for remaining timezone differences. | | [`os.tmpname()`](https://www.lua.org/manual/5.2/manual.html#pdf-os.tmpname) | Returns a temporary pathname. | **Host-dependent:** default `Path.GetTempFileName` creates an empty file while choosing the name. The caller owns cleanup; unlike `io.tmpfile`, this API does not attach delete-on-close behavior. The name uses the state's string representation. | ## Date conversion details @@ -33,4 +33,6 @@ Formatting rules and remaining differences: Timestamp conversion is bounded by .NET date/time ranges. For byte-string states, format literals remain byte-string units and managed timezone names are encoded as text. Use the official link for the complete Lua/C contract and this list for the implemented behavior. +Date-table normalization uses the first day of the normalized year/month, then adds the day and time offsets. Both that anchor and the resulting date must fit .NET's years 1–9999. C `mktime` ranges and integer field widths depend on the platform; managed conversion does not emulate every C runtime's limits. + During a repeated local hour, a truthy `isdst` selects the daylight-saving occurrence and false selects standard time. Nil/omitted `isdst` uses .NET's standard-time default for ambiguous dates. This preserves `os.time(os.date('*t', timestamp))` across the repeated hour, including negative daylight-saving adjustments. **Remaining limitation:** for an unambiguous date, `isdst` does not force a seasonal correction as C `mktime` can; the timezone's applicable offset is used. diff --git a/src/Lua/IO/StandardIOStream.cs b/src/Lua/IO/StandardIOStream.cs index 910ec443..85088a69 100644 --- a/src/Lua/IO/StandardIOStream.cs +++ b/src/Lua/IO/StandardIOStream.cs @@ -5,6 +5,8 @@ /// sealed class StandardIOStream(ILuaStream innerStream) : ILuaStream { + internal ILuaStream InnerStream => innerStream; + public LuaFileOpenMode Mode => innerStream.Mode; public bool IsOpen => innerStream.IsOpen; diff --git a/src/Lua/Internal/LuaGlobalState.cs b/src/Lua/Internal/LuaGlobalState.cs index c3983ebc..3301f2df 100644 --- a/src/Lua/Internal/LuaGlobalState.cs +++ b/src/Lua/Internal/LuaGlobalState.cs @@ -39,6 +39,7 @@ sealed class LuaGlobalState LuaTable? booleanMetatable; LuaTable? functionMetatable; LuaTable? stateMetatable; + LuaTable? lightUserDataMetatable; LuaTable? fileHandleMetatable; internal LuaTable FileHandleMetatable => fileHandleMetatable ??= FileHandle.CreateMetatable(); @@ -69,6 +70,7 @@ internal bool TryGetMetatable(LuaValue value, [NotNullWhen(true)] out LuaTable? LuaValueType.Number => numberMetatable, LuaValueType.Function => functionMetatable, LuaValueType.Thread => stateMetatable, + LuaValueType.LightUserData => lightUserDataMetatable, LuaValueType.UserData => value.UnsafeRead().Metatable, LuaValueType.Table => value.UnsafeRead().Metatable, _ => null, @@ -100,6 +102,9 @@ internal void SetMetatable(LuaValue value, LuaTable metatable) case LuaValueType.Thread: stateMetatable = metatable; break; + case LuaValueType.LightUserData: + lightUserDataMetatable = metatable; + break; case LuaValueType.UserData: value.UnsafeRead().Metatable = metatable; break; diff --git a/src/Lua/LuaStateExtensions.cs b/src/Lua/LuaStateExtensions.cs index 0f438050..747603ac 100644 --- a/src/Lua/LuaStateExtensions.cs +++ b/src/Lua/LuaStateExtensions.cs @@ -23,15 +23,30 @@ public static LuaClosure Load(this LuaState state, LuaModule module) : state.Load(module.ReadText(), module.Name); } - /// Loads a file whose name is .NET text, including in byte-string mode. + /// Loads a file whose name is .NET text, or standard input when null. public static async ValueTask LoadFileAsync( this LuaState state, - string fileName, + string? fileName, string mode, LuaTable? environment, CancellationToken cancellationToken ) { + if (fileName is null) + { + var input = state.Platform.StandardIO.Input; + if (input is StandardIOStream standardInput) + input = standardInput.InnerStream; + return await LoadStreamAsync( + state, + input, + "=stdin", + mode, + environment, + cancellationToken + ); + } + var name = state.Platform.EncodeText("@" + fileName); using var stream = await state.GlobalState.Platform.FileSystem.Open( fileName, @@ -39,6 +54,18 @@ CancellationToken cancellationToken cancellationToken ); + return await LoadStreamAsync(state, stream, name, mode, environment, cancellationToken); + } + + static async ValueTask LoadStreamAsync( + LuaState state, + ILuaStream stream, + string name, + string mode, + LuaTable? environment, + CancellationToken cancellationToken + ) + { LuaClosure closure; if (stream is ILuaByteStream byteStream) { @@ -60,15 +87,16 @@ CancellationToken cancellationToken await byteStream.ReadBytesAsync(source, cancellationToken); var bytes = source.WrittenSpan; var chunk = BomUtility.SkipUtf8Bom(bytes); + var text = bytes; if (!chunk.IsEmpty && chunk[0] == '#') { var newline = chunk.IndexOf((byte)'\n'); + text = newline < 0 ? default : chunk[newline..]; chunk = newline < 0 ? default : chunk[(newline + 1)..]; } - // Text keeps its initial comment for line numbers. Binary chunks - // begin after the optional BOM and first-line comment. + // Keep the comment's newline for text source line numbers. closure = state.LoadCore( - !chunk.IsEmpty && chunk[0] == '\e' ? chunk : bytes, + !chunk.IsEmpty && chunk[0] == '\e' ? chunk : text, name, mode, environment @@ -82,7 +110,15 @@ CancellationToken cancellationToken else { var source = await stream.ReadAllAsync(cancellationToken); - closure = state.LoadCore(source, name, environment); + var text = source.AsSpan(); + if (!text.IsEmpty && text[0] == '\uFEFF') + text = text[1..]; + if (!text.IsEmpty && text[0] == '#') + { + var newline = text.IndexOf('\n'); + text = newline < 0 ? default : text[newline..]; + } + closure = state.LoadCore(text, name, environment); } return closure; diff --git a/src/Lua/Standard/BasicLibrary.cs b/src/Lua/Standard/BasicLibrary.cs index 14906bbc..ece3fa77 100644 --- a/src/Lua/Standard/BasicLibrary.cs +++ b/src/Lua/Standard/BasicLibrary.cs @@ -131,10 +131,10 @@ public async ValueTask DoFile( CancellationToken cancellationToken ) { - var arg0 = context.GetStringArgument(0); + var arg0 = context.HasArgument(0) ? context.GetStringArgument(0) : null; context.State.Stack.PopUntil(context.ReturnFrameBase); var closure = await context.State.LoadFileAsync( - context.State.Platform.DecodeText(arg0), + arg0 is null ? null : context.State.Platform.DecodeText(arg0), "bt", null, cancellationToken @@ -234,7 +234,7 @@ public async ValueTask LoadFile( CancellationToken cancellationToken ) { - var arg0 = context.GetStringArgument(0); + var arg0 = context.HasArgument(0) ? context.GetStringArgument(0) : null; var mode = context.HasArgument(1) ? context.GetStringArgument(1) : "bt"; var hasEnvironment = context.ArgumentCount > 2; var environment = hasEnvironment ? context.GetArgument(2) : LuaValue.Nil; @@ -243,7 +243,7 @@ CancellationToken cancellationToken try { var closure = await context.State.LoadFileAsync( - context.State.Platform.DecodeText(arg0), + arg0 is null ? null : context.State.Platform.DecodeText(arg0), mode, null, cancellationToken diff --git a/src/Lua/Standard/Internal/DateTimeHelper.cs b/src/Lua/Standard/Internal/DateTimeHelper.cs index d9e11d4a..556f66da 100644 --- a/src/Lua/Standard/Internal/DateTimeHelper.cs +++ b/src/Lua/Standard/Internal/DateTimeHelper.cs @@ -1,6 +1,8 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text; +using Lua.Internal.CompilerServices; +using Lua.Runtime; namespace Lua.Standard.Internal; @@ -45,44 +47,140 @@ public static double GetUnixTimeFromLocalTime(DateTime dateTime, TimeZoneInfo zo return new DateTimeOffset(dateTime, offset).ToUnixTimeSeconds(); } - public static DateTime ParseTimeTable(LuaState state, LuaTable table) + public static LuaValue GetTime(LuaState state, LuaTable table) { - static int GetTimeField( - LuaState state, - LuaTable table, - string key, - bool required = true, - int defaultValue = 0 - ) - { - if (!table.TryGetValue(key, out var value)) - { - if (required) - { - throw new LuaRuntimeException(state, $"field '{key}' missing in date table"); - } - else - { - return defaultValue; - } - } + var sec = GetTimeField(state, table["sec"], "sec", 0); + var min = GetTimeField(state, table["min"], "min", 0); + var hour = GetTimeField(state, table["hour"], "hour", 12); + var day = GetTimeField(state, table["day"], "day"); + var month = GetTimeField(state, table["month"], "month"); + var year = GetTimeField(state, table["year"], "year"); + var zone = state.Platform.TimeProvider.LocalTimeZone; + return CreateTime( + zone, + sec, + min, + hour, + day, + month, + year, + zone.SupportsDaylightSavingTime && table["isdst"].ToBoolean() + ); + } - if (value.TryRead(out var d) && MathEx.IsInteger(d)) + [AsyncMethodBuilder(typeof(LightAsyncValueTaskMethodBuilder<>))] + public static async ValueTask GetTimeAsync( + LuaState state, + LuaTable table, + CancellationToken cancellationToken + ) + { + var sec = GetTimeField( + state, + await ReadFieldAsync(state, table, "sec", cancellationToken), + "sec", + 0 + ); + var min = GetTimeField( + state, + await ReadFieldAsync(state, table, "min", cancellationToken), + "min", + 0 + ); + var hour = GetTimeField( + state, + await ReadFieldAsync(state, table, "hour", cancellationToken), + "hour", + 12 + ); + var day = GetTimeField( + state, + await ReadFieldAsync(state, table, "day", cancellationToken), + "day" + ); + var month = GetTimeField( + state, + await ReadFieldAsync(state, table, "month", cancellationToken), + "month" + ); + var year = GetTimeField( + state, + await ReadFieldAsync(state, table, "year", cancellationToken), + "year" + ); + var isDst = (await ReadFieldAsync(state, table, "isdst", cancellationToken)).ToBoolean(); + return CreateTime( + state.Platform.TimeProvider.LocalTimeZone, + sec, + min, + hour, + day, + month, + year, + isDst + ); + } + + static LuaValue CreateTime( + TimeZoneInfo zone, + int sec, + int min, + int hour, + int day, + int month, + int year, + bool isDst + ) + { + try + { + var normalizedYear = Math.DivRem( + (long)year * 12 + month - 1, + 12, + out var normalizedMonth + ); + if (normalizedMonth < 0) { - return (int)d; + normalizedMonth += 12; + normalizedYear--; } - throw new LuaRuntimeException(state, $"field '{key}' is not an integer"); + var date = new DateTime(checked((int)normalizedYear), (int)normalizedMonth + 1, 1); + date = date.AddSeconds(((long)day - 1) * 86400 + hour * 3600L + min * 60L + sec); + var time = GetUnixTimeFromLocalTime(date, zone, isDst); + return time == -1 ? LuaValue.Nil : time; + } + catch (ArgumentOutOfRangeException) + { + return LuaValue.Nil; + } + catch (OverflowException) + { + return LuaValue.Nil; } + } - var day = GetTimeField(state, table, "day"); - var month = GetTimeField(state, table, "month"); - var year = GetTimeField(state, table, "year"); - var sec = GetTimeField(state, table, "sec", false, 0); - var min = GetTimeField(state, table, "min", false, 0); - var hour = GetTimeField(state, table, "hour", false, 12); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int GetTimeField(LuaState state, LuaValue value, string key, int defaultValue = -1) + { + if (value.Type != LuaValueType.Nil && value.TryReadDouble(out var number)) + return (int)(long)number; + if (defaultValue >= 0) + return defaultValue; + throw new LuaRuntimeException(state, $"field '{key}' missing in date table"); + } - return new(year, month, day, hour, min, sec); + static ValueTask ReadFieldAsync( + LuaState state, + LuaTable table, + string key, + CancellationToken cancellationToken + ) + { + var value = table[key]; + if (value.Type != LuaValueType.Nil || table.Metatable is null) + return new(value); + return LuaVirtualMachine.ExecuteGetTableSlowPath(state, table, key, cancellationToken); } public static string StrFTime( diff --git a/src/Lua/Standard/OperatingSystemLibrary.cs b/src/Lua/Standard/OperatingSystemLibrary.cs index b900af79..ef3e70a6 100644 --- a/src/Lua/Standard/OperatingSystemLibrary.cs +++ b/src/Lua/Standard/OperatingSystemLibrary.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; +using Lua.Internal.CompilerServices; using Lua.Standard.Internal; namespace Lua.Standard; @@ -215,17 +217,9 @@ CancellationToken cancellationToken if (context.HasArgument(0)) { var table = context.GetArgument(0); - var date = DateTimeHelper.ParseTimeTable(context.State, table); - var zone = context.State.Platform.TimeProvider.LocalTimeZone; - return new( - context.Return( - DateTimeHelper.GetUnixTimeFromLocalTime( - date, - zone, - zone.SupportsDaylightSavingTime && table["isdst"].ToBoolean() - ) - ) - ); + if (table.Metatable is not null) + return TimeWithMetatableAsync(context, table, cancellationToken); + return new(context.Return(DateTimeHelper.GetTime(context.State, table))); } else { @@ -238,6 +232,26 @@ CancellationToken cancellationToken } } + [AsyncMethodBuilder(typeof(LightAsyncValueTaskMethodBuilder<>))] + static async ValueTask TimeWithMetatableAsync( + LuaFunctionExecutionContext context, + LuaTable table, + CancellationToken cancellationToken + ) + { + context.State.NonYieldableCallCount++; + try + { + return context.Return( + await DateTimeHelper.GetTimeAsync(context.State, table, cancellationToken) + ); + } + finally + { + context.State.NonYieldableCallCount--; + } + } + public ValueTask TmpName( LuaFunctionExecutionContext context, CancellationToken cancellationToken diff --git a/tests/Lua.Tests/LightUserDataMetatableTests.cs b/tests/Lua.Tests/LightUserDataMetatableTests.cs new file mode 100644 index 00000000..db2de3e5 --- /dev/null +++ b/tests/Lua.Tests/LightUserDataMetatableTests.cs @@ -0,0 +1,60 @@ +using Lua.Standard; + +namespace Lua.Tests; + +public class LightUserDataMetatableTests +{ + [Test] + public async Task MetatableIsSharedByLightUserDataAndCoroutines() + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + state.Environment["host"] = LuaValue.FromObject(new object()); + await state.DoStringAsync( + """ + local value=1 + local a=debug.upvalueid(function() return value end,1) + local mt={__index={answer=42},__metatable='protected', + __newindex=function(_,k,v) value=v end, + __add=function() return 12 end,__unm=function() return -12 end, + __len=function() return 7 end,__concat=function() return 'joined' end, + __lt=function() return true end,__le=function() return true end, + __eq=function() error('light userdata equality must use identity') end, + __call=function(_,v) return v end,__tostring=function() return 'token' end} + assert(debug.setmetatable(a,mt)==a) + assert(debug.getmetatable(host)==mt and getmetatable(host)=='protected') + assert(a~=host and a==a) + assert(host.answer==42 and host+1==12 and -host==-12 and #host==7) + assert(host..'x'=='joined' and host<1 and host<=1 and host(9)==9) + assert(tostring(host)=='token') + host.answer=17 + assert(value==17) + local co=coroutine.create(function() + assert(debug.getmetatable(a)==mt) + debug.setmetatable(host,{__index={answer=99}}) + end) + assert(coroutine.resume(co)) + assert(a.answer==99) + debug.setmetatable(a,nil) + assert(debug.getmetatable(host)==nil and getmetatable(a)==nil) + assert(not pcall(function() return host.answer end)) + """ + ); + } + + [Test] + public async Task IndependentStatesHaveIndependentLightUserDataMetatables() + { + var token = LuaValue.FromObject(new object()); + using var first = LuaState.Create(); + using var second = LuaState.Create(); + foreach (var state in new[] { first, second }) + { + state.OpenStandardLibraries(); + state.Environment["token"] = token; + } + await first.DoStringAsync("debug.setmetatable(token,{__index={answer=42}})"); + await second.DoStringAsync("assert(debug.getmetatable(token)==nil)"); + await first.DoStringAsync("assert(token.answer==42)"); + } +} diff --git a/tests/Lua.Tests/StandardInputLoadingTests.cs b/tests/Lua.Tests/StandardInputLoadingTests.cs new file mode 100644 index 00000000..1252b832 --- /dev/null +++ b/tests/Lua.Tests/StandardInputLoadingTests.cs @@ -0,0 +1,151 @@ +using System.Text; +using Lua.IO; +using Lua.Runtime; +using Lua.Standard; + +namespace Lua.Tests; + +public class StandardInputLoadingTests +{ + sealed class StandardIO(ILuaStream input) : ILuaStandardIO + { + public ILuaStream Input => input; + public ILuaStream Output => input; + public ILuaStream Error => input; + } + + static LuaState CreateState(ILuaStream input, LuaStringMode mode) + { + var state = LuaState.Create(); + state.Platform = state.Platform with + { + StandardIO = new StandardIO(input), + StringMode = mode, + }; + state.OpenBasicLibrary(); + state.OpenDebugLibrary(); + state.OpenStringLibrary(); + return state; + } + + [TestCase("return assert(loadfile())()", LuaStringMode.Utf16)] + [TestCase("return assert(loadfile(nil))()", LuaStringMode.Bytes)] + [TestCase("return dofile()", LuaStringMode.Utf16)] + [TestCase("return dofile(nil)", LuaStringMode.Bytes)] + public async Task OmittedAndNilFilenamesReadStandardInput(string source, LuaStringMode mode) + { + using var bytes = new MemoryStream( + Encoding.UTF8.GetBytes("\uFEFF#!/lua\nreturn 42,nil,'é'") + ); + using var input = new LuaStream(LuaFileOpenMode.Read, bytes, mode); + using var state = CreateState(new StandardIOStream(input), mode); + var result = await state.DoStringAsync(source); + Assert.That( + result, + Is.EqualTo(new LuaValue[] { 42, LuaValue.Nil, state.Platform.EncodeText("é") }) + ); + Assert.That(bytes.CanRead, Is.True); + } + + [TestCase("{answer=42}", "return answer", 42)] + [TestCase("nil", "\uFEFF#!/lua\nreturn _ENV", null)] + public async Task TextProvidersSupportEnvironmentsAndStdinChunkNames( + string environment, + string source, + int? expected + ) + { + using var input = ILuaStream.CreateFromString(source); + using var state = CreateState(input, LuaStringMode.Utf16); + var result = await state.DoStringAsync( + "local f=assert(loadfile(nil,'t'," + + environment + + ")); assert(debug.getinfo(f,'S').source=='=stdin'); return f()" + ); + Assert.That( + result, + Is.EqualTo(new[] { expected.HasValue ? new LuaValue(expected.Value) : LuaValue.Nil }) + ); + Assert.That(input.IsOpen, Is.True); + } + + [TestCase(false, LuaStringMode.Utf16)] + [TestCase(true, LuaStringMode.Bytes)] + public async Task BinaryStandardInputPreservesBytesAndHonorsMode( + bool binaryAllowed, + LuaStringMode mode + ) + { + using var compiler = LuaState.Create(); + var chunk = compiler.Load("return 'binary',42", "@binary").Proto.ToBytecode(); + using var bytes = new MemoryStream(chunk); + using var input = new LuaStream(LuaFileOpenMode.Read, bytes, mode); + using var state = CreateState(new StandardIOStream(input), mode); + var result = await state.DoStringAsync( + binaryAllowed + ? "return assert(loadfile(nil,'b'))()" + : "local f,err=loadfile(nil,'t'); assert(f==nil and err:find('a binary chunk',1,true)); return true" + ); + Assert.That( + result, + Is.EqualTo(binaryAllowed ? new LuaValue[] { "binary", 42 } : new LuaValue[] { true }) + ); + Assert.That(bytes.CanRead, Is.True); + } + + [TestCase("local f,err=loadfile(); assert(f==nil and err:find('stdin',1,true))")] + [TestCase("local ok,err=pcall(dofile); assert(not ok and err:find('stdin',1,true))")] + public async Task SyntaxErrorsPreserveInputOwnership(string source) + { + using var bytes = new MemoryStream(Encoding.UTF8.GetBytes("#!/lua\nlocal =")); + using var input = new LuaStream(LuaFileOpenMode.Read, bytes); + using var state = CreateState(input, LuaStringMode.Utf16); + await state.DoStringAsync(source); + Assert.That(bytes.CanRead, Is.True); + } + + [TestCase("")] + [TestCase("#!/lua")] + public async Task EmptyStandardInputCompilesAnEmptyChunk(string source) + { + using var input = ILuaStream.CreateFromString(source); + using var state = CreateState(input, LuaStringMode.Utf16); + var result = await state.DoStringAsync("return assert(loadfile())()"); + Assert.That(result, Is.Empty); + Assert.That(input.IsOpen, Is.True); + } + + [Test] + public async Task StandardInputIsIndependentOfIoLibrarySelections() + { + using var input = ILuaStream.CreateFromString("return 42"); + using var other = ILuaStream.CreateFromString("return 99"); + using var state = CreateState(input, LuaStringMode.Utf16); + state.OpenIOLibrary(); + state.Environment["other"] = LuaValue.FromUserData( + new FileHandle(other, state.GlobalState.FileHandleMetatable) + ); + var result = await state.DoStringAsync( + """ + io.input(other) + io.stdin=nil + return assert(loadfile())() + """ + ); + Assert.That(result, Is.EqualTo(new LuaValue[] { 42 })); + } + + [Test] + public async Task BinaryModeRejectsTextProvidersWithoutClosingThem() + { + using var input = ILuaStream.CreateFromString("return 42"); + using var state = CreateState(input, LuaStringMode.Utf16); + await state.DoStringAsync( + """ + local f,err=loadfile(nil,'b') + assert(f==nil and err:find('a text chunk',1,true)) + """ + ); + Assert.That(input.IsOpen, Is.True); + } +} diff --git a/tests/Lua.Tests/TimeTableTests.cs b/tests/Lua.Tests/TimeTableTests.cs new file mode 100644 index 00000000..e0603c09 --- /dev/null +++ b/tests/Lua.Tests/TimeTableTests.cs @@ -0,0 +1,133 @@ +using Lua.Platforms; +using Lua.Standard; +using Microsoft.Extensions.Time.Testing; + +namespace Lua.Tests; + +public class TimeTableTests +{ + static LuaState CreateState() + { + var provider = new FakeTimeProvider(); + provider.SetLocalTimeZone(TimeZoneInfo.Utc); + var state = LuaState.Create(LuaPlatform.Default with { TimeProvider = provider }); + state.OpenStandardLibraries(); + return state; + } + + [TestCase("year=2000,month=3,day=0", "2000-02-29 12:00:00")] + [TestCase("year=2000,month=13,day=1", "2001-01-01 12:00:00")] + [TestCase("year=2000,month=-12,day=1", "1998-12-01 12:00:00")] + [TestCase("year=2000,month=2,day=30,hour=24,min=-1,sec=61", "2000-03-02 00:00:01")] + [TestCase("year='2000.9',month='1.9',day=2.9,hour=3.9,min=4.9,sec=5.9", "2000-01-02 03:04:05")] + [TestCase("year=2000,month=1,day=2,hour=-0.9,min=-1.9,sec=-1.9", "2000-01-01 23:58:59")] + [TestCase("year=2000,month=1,day=2,hour=false,min={},sec='invalid'", "2000-01-02 12:00:00")] + public async Task FieldsAreConvertedAndNormalizedWithoutUpdatingTheTable( + string fields, + string expected + ) + { + using var state = CreateState(); + var result = await state.DoStringAsync( + "local t={" + + fields + + "}; local copy={}; for k,v in pairs(t) do copy[k]=v end; " + + "local timestamp=os.time(t); for k,v in pairs(t) do assert(copy[k]==v) end; " + + "for k,v in pairs(copy) do assert(t[k]==v) end; return os.date('!%Y-%m-%d %H:%M:%S',timestamp)" + ); + Assert.That(result, Is.EqualTo(new LuaValue[] { expected })); + } + + [TestCase("day")] + [TestCase("month")] + [TestCase("year")] + public async Task InvalidRequiredFieldsRaiseLuaErrors(string field) + { + using var state = CreateState(); + state.Environment["field"] = field; + await state.DoStringAsync( + """ + local t={year=2000,month=1,day=1} + for _,v in ipairs({false,{},'invalid'}) do + t[field]=v + local ok,err=pcall(os.time,t) + assert(not ok and err:find("field '"..field.."' missing in date table",1,true)) + end + t[field]=nil + assert(not pcall(os.time,t)) + """ + ); + } + + [Test] + public async Task FieldsUseIndexMetamethodsInLua52Order() + { + using var state = CreateState(); + await state.DoStringAsync( + """ + local fields={sec=5,min=4,hour=3,day=2,month=1,year=2000,isdst=false} + local order={} + local t=setmetatable({}, {__index=function(_,key) + order[#order+1]=key + return fields[key] + end}) + assert(os.date('!%Y-%m-%d %H:%M:%S',os.time(t))=='2000-01-02 03:04:05') + assert(table.concat(order,',')=='sec,min,hour,day,month,year,isdst') + assert(next(t)==nil) + assert(os.time(setmetatable({}, {__index=fields}))==os.time(fields)) + local marker={} + local ok,err=pcall(os.time,setmetatable({}, {__index=function() error(marker) end})) + assert(not ok and err==marker) + local co=coroutine.create(function() + local ok=pcall(os.time,setmetatable({}, {__index=coroutine.yield})) + assert(not ok) + coroutine.yield('restored') + end) + local ok,value=coroutine.resume(co) + assert(ok and value=='restored') + """ + ); + } + + [Test] + public async Task HostIndexCallbacksCanAwait() + { + using var state = CreateState(); + state.Environment["read"] = new LuaFunction( + "read", + async (context, _) => + { + var key = context.GetArgument(1); + await Task.Yield(); + return context.Return( + key switch + { + "year" => 2000, + "month" => 1, + "day" => 2, + _ => LuaValue.Nil, + } + ); + } + ); + await state.DoStringAsync( + """ + local t=setmetatable({}, {__index=read}) + assert(os.time(t)==946814400) + """ + ); + } + + [Test] + public async Task UnrepresentableDatesReturnNil() + { + using var state = CreateState(); + await state.DoStringAsync( + """ + assert(os.time{year=10000,month=1,day=1}==nil) + assert(os.time{year=2000,month=1,day=2147483647}==nil) + assert(os.time{year=1969,month=12,day=31,hour=23,min=59,sec=59}==nil) + """ + ); + } +} From ea3031da245528be1d6a219a50e4f2e3c08d59d5 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 31 Oct 2026 11:18:02 +0900 Subject: [PATCH 7/7] docs: correct shared light-userdata metatable description --- docs/stdlib/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stdlib/README.md b/docs/stdlib/README.md index d44aa1e0..5305a2f1 100644 --- a/docs/stdlib/README.md +++ b/docs/stdlib/README.md @@ -69,7 +69,7 @@ The .NET garbage collector owns object lifetimes. Lua weak-table `__mode` semant C# objects can be exposed as userdata, and host functions are wrapped as `LuaFunction` values. Their identity strings and debug metadata describe managed objects rather than native pointers. Host-frozen tables, including generated C# metatables, reject mutation even through raw table operations; this keeps their published interop definitions immutable. -Metatable-bearing host userdata implements `ILuaUserData`. Light-userdata object tokens have no metatable slot in the current managed metatable dispatch, so `debug.setmetatable` does not attach one to those tokens. +Host userdata implementing `ILuaUserData` has a per-object metatable. Light-userdata object tokens, including `debug.upvalueid` results, instead share one type metatable within a global state and its coroutines, as in Lua 5.2. `debug.setmetatable` can replace or remove that shared metatable; independent states have separate metatables. ## Scope