From 9b43a011fc4df1bc94dc0974dc101236850d1dc5 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:34:09 +0900 Subject: [PATCH] feat: add platform option for Lua 5.2 numeric formatting Add LuaNumberFormat.DotNet and LuaNumberFormat.Lua on LuaPlatform, keeping the existing invariant managed default and public LuaValue.ToString(). Route Lua numeric text through shared string/span formatting so the Lua option uses C-locale %.14g and portable signed inf/nan spellings across conversion, concatenation, replacements, explicit-base tonumber and I/O. Explicit floating-point formats keep their requested finite precision. Normalize Windows CRT NaN classifiers instead of making serialized text host-dependent. Preserve NaN bits, sign, arithmetic and string parsing. Document the policy and precision implications in both READMEs. Validation: Release solution build passes without warnings for all runtime targets. Full runtime suites pass on .NET 8/10, plus 73 generator tests; all 58 numeric policy cases pass on both frameworks. CSharpier and benchmark --verify pass. Native Lua 5.2 comparisons cover 18,567 implicit conversions and 15 I/O cases with zero differences after NaN suffix normalization. Explicit format comparisons identify existing Windows CRT hex-format spelling/flag differences, which are intentionally not emulated. Paired .NET 10 default conversion benchmarks remain within about 4% of main, with unchanged allocations; Lua mode's shorter 1/3 output is about 4-16% faster in the sampled conversion workloads. --- README.md | 18 ++ README_JA.md | 8 + src/Lua/Internal/HexConverter.cs | 5 +- src/Lua/Internal/NumberFormatter.cs | 53 +++++ src/Lua/LuaNumberFormat.cs | 11 + src/Lua/LuaValue.cs | 9 +- src/Lua/Platforms/LuaPlatform.cs | 8 + src/Lua/Runtime/LuaVirtualMachine.cs | 21 +- src/Lua/Standard/BasicLibrary.cs | 2 +- src/Lua/Standard/IOLibrary.cs | 8 +- src/Lua/Standard/Internal/IOHelper.cs | 8 +- src/Lua/Standard/StringLibrary.cs | 34 ++- src/Lua/Standard/TableLibrary.cs | 10 +- tests/Lua.Tests/NumericTextPolicyTests.cs | 248 ++++++++++++++++++++++ 14 files changed, 409 insertions(+), 34 deletions(-) create mode 100644 src/Lua/Internal/NumberFormatter.cs create mode 100644 src/Lua/LuaNumberFormat.cs create mode 100644 tests/Lua.Tests/NumericTextPolicyTests.cs diff --git a/README.md b/README.md index aadbbb1b..0bbf117c 100644 --- a/README.md +++ b/README.md @@ -606,6 +606,24 @@ var state = LuaState.Create(platform); These are used for `require`, `print`, `dofile`, and the `os` module. +### Number formatting + +`LuaPlatform.NumberFormat` defaults to `LuaNumberFormat.DotNet`, preserving invariant .NET numeric text. Select `LuaNumberFormat.Lua` for Lua 5.2's default C-locale `%.14g` conversion: 14 significant digits, lowercase exponents, signed zero, and signed `inf`/`nan`. + +```cs +using Lua.Platforms; + +using var state = LuaState.Create(LuaPlatform.Default with +{ + NumberFormat = LuaNumberFormat.Lua +}); +await state.DoStringAsync("print(1/3)"); // 0.33333333333333 +``` + +The option applies to `tostring` (including numeric `__tostring` results), `%s`/`%q`, concatenation, `table.concat`, numeric `gsub` replacements, explicit-base `tonumber` numeric inputs, and numeric file/standard output. Coroutines inherit it, independently of `StringMode`. Explicit floating-point formats such as `%.17g` keep their requested precision; the option selects their infinity/NaN spellings. + +Lua 5.2 delegates formatting to the C runtime, so exact special-value text varies between systems. `Lua` uses portable signed `nan` rather than Windows CRT suffixes such as `nan(ind)` or `nan(snan)`. Arithmetic, NaN bits, parsing of numeric strings, and public C# `LuaValue.ToString()` are unchanged. Formatting to 14 digits can lose round-trip precision; for example, `tonumber(1e14,10)` returns `nil` in `Lua` mode because the intermediate text is `1e+14`. + ## Exception Handling Lua script runtime exceptions throw exceptions that inherit from `LuaException`. You can catch these to handle errors during execution. diff --git a/README_JA.md b/README_JA.md index 18322f95..1b28b8f0 100644 --- a/README_JA.md +++ b/README_JA.md @@ -556,6 +556,14 @@ var state = LuaState.Create(platform); これらは`require`, `print`, `dofile`や`os`モジュールなどで使用されます。 +### 数値の書式 + +`LuaPlatform.NumberFormat`の既定値は`LuaNumberFormat.DotNet`で、従来のカルチャ非依存の.NET書式を使用します。`LuaPlatform.Default with { NumberFormat = LuaNumberFormat.Lua }`を`LuaState.Create`に渡すと、Lua 5.2のCロケールの既定値`%.14g`に合わせ、有効数字14桁、小文字の指数、符号付きのゼロと`inf`/`nan`を使用します。 + +`tostring`、数値を返す`__tostring`、`%s`/`%q`、連結、`table.concat`、`gsub`の数値置換、基数を指定した`tonumber`の数値引数、数値のファイル・標準出力に適用されます。コルーチンにも継承され、`StringMode`とは独立しています。`%.17g`などの明示的な浮動小数点書式の精度は維持され、特殊値の表記だけが切り替わります。 + +Lua 5.2の実際の出力はCランタイムに依存します。`Lua`ではWindows固有の`nan(ind)`や`nan(snan)`を付けず、符号付きの`nan`に統一します。演算、NaNのビット列、数値文字列の解析、C#の`LuaValue.ToString()`は変わりません。14桁への変換で精度が失われる場合があり、例えば`Lua`モードの`tonumber(1e14,10)`は途中の文字列が`1e+14`となるため`nil`を返します。 + ## 例外処理 Luaスクリプトの実行時例外は`LuaRuntimeException`を継承した例外をスローします。これをcatchすることでエラー時の処理を行うことができます。 diff --git a/src/Lua/Internal/HexConverter.cs b/src/Lua/Internal/HexConverter.cs index 453ee396..94395688 100644 --- a/src/Lua/Internal/HexConverter.cs +++ b/src/Lua/Internal/HexConverter.cs @@ -148,13 +148,14 @@ internal static string FormatDouble( double value, int precision, bool alternateForm, - bool upperCase + bool upperCase, + LuaNumberFormat numberFormat ) { string text; if (double.IsNaN(value) || double.IsInfinity(value)) { - text = value.ToString(CultureInfo.InvariantCulture); + return NumberFormatter.FormatSpecial(value, numberFormat, upperCase); } else { diff --git a/src/Lua/Internal/NumberFormatter.cs b/src/Lua/Internal/NumberFormatter.cs new file mode 100644 index 00000000..1d7d348c --- /dev/null +++ b/src/Lua/Internal/NumberFormatter.cs @@ -0,0 +1,53 @@ +using System.Globalization; + +namespace Lua.Internal; + +static class NumberFormatter +{ + public static string Format(double value, LuaNumberFormat format) + { + if (format == LuaNumberFormat.Lua && !MathEx.IsFinite(value)) + return FormatSpecial(value, format); + return value.ToString( + format == LuaNumberFormat.Lua ? "g14" : null, + CultureInfo.InvariantCulture + ); + } + + public static bool TryFormat( + double value, + Span destination, + out int written, + LuaNumberFormat format + ) + { + if (format == LuaNumberFormat.Lua && !MathEx.IsFinite(value)) + { + var text = FormatSpecial(value, format); + written = text.Length; + if (text.AsSpan().TryCopyTo(destination)) + return true; + written = 0; + return false; + } + return value.TryFormat( + destination, + out written, + format == LuaNumberFormat.Lua ? "g14" : default, + CultureInfo.InvariantCulture + ); + } + + public static string FormatSpecial(double value, LuaNumberFormat format, bool upperCase = false) + { + string text; + if (format == LuaNumberFormat.Lua) + { + var negative = BitConverter.DoubleToInt64Bits(value) < 0; + text = double.IsNaN(value) ? (negative ? "-nan" : "nan") : (negative ? "-inf" : "inf"); + } + else + text = value.ToString(CultureInfo.InvariantCulture); + return upperCase ? text.ToUpperInvariant() : text; + } +} diff --git a/src/Lua/LuaNumberFormat.cs b/src/Lua/LuaNumberFormat.cs new file mode 100644 index 00000000..40c9c338 --- /dev/null +++ b/src/Lua/LuaNumberFormat.cs @@ -0,0 +1,11 @@ +namespace Lua; + +/// The numeric text representation used by Lua execution. +public enum LuaNumberFormat +{ + /// Invariant .NET formatting. This is the default. + DotNet, + + /// Lua 5.2's default C-locale 14 significant digits and portable signed inf/nan spellings. + Lua, +} diff --git a/src/Lua/LuaValue.cs b/src/Lua/LuaValue.cs index cba8fb21..b4f71adc 100644 --- a/src/Lua/LuaValue.cs +++ b/src/Lua/LuaValue.cs @@ -684,6 +684,9 @@ public override string ToString() }; } + internal string ToLuaString(LuaNumberFormat format) => + Type == LuaValueType.Number ? NumberFormatter.Format(value, format) : ToString(); + public string TypeToString() { return ToString(Type); @@ -766,7 +769,7 @@ CancellationToken cancellationToken } else { - context.State.Stack.Push(ToString()); + context.State.Stack.Push(ToLuaString(context.State.Platform.NumberFormat)); return new(1); } @@ -789,7 +792,9 @@ CancellationToken cancellationToken stack.SetTop(top + 1); // Lua 5.2 converts a numeric __tostring result to text. stack.FastGet(top) = - result.Type == LuaValueType.Number ? new LuaValue(result.ToString()) : result; + result.Type == LuaValueType.Number + ? new LuaValue(result.ToLuaString(state.Platform.NumberFormat)) + : result; return 1; } catch diff --git a/src/Lua/Platforms/LuaPlatform.cs b/src/Lua/Platforms/LuaPlatform.cs index 249f5908..59290a69 100644 --- a/src/Lua/Platforms/LuaPlatform.cs +++ b/src/Lua/Platforms/LuaPlatform.cs @@ -29,6 +29,14 @@ TimeProvider TimeProvider /// public LuaStringMode StringMode { get; init; } + /// + /// Controls numeric text produced by Lua execution, including implicit conversions and I/O. + /// Defaults to . Explicit floating-point formats retain + /// their requested precision; this option selects their infinity and NaN spellings. + /// Does not change , arithmetic or parsing of numeric strings. + /// + public LuaNumberFormat NumberFormat { get; init; } + /// /// Converts .NET text to this platform's Lua string representation. /// Returns the text unchanged in UTF-16 mode or encodes it as UTF-8 in byte mode. diff --git a/src/Lua/Runtime/LuaVirtualMachine.cs b/src/Lua/Runtime/LuaVirtualMachine.cs index 2ab9174d..c019be60 100644 --- a/src/Lua/Runtime/LuaVirtualMachine.cs +++ b/src/Lua/Runtime/LuaVirtualMachine.cs @@ -1208,7 +1208,7 @@ static bool Concat(VirtualMachineExecutionContext context) var c = instruction.C; stack.SetTop(context.FrameBase + c + 1); var a = instruction.A; - var total = ConcatStrings(stack, c - b + 1); + var total = ConcatStrings(stack, c - b + 1, context.State.Platform.NumberFormat); if (total == 1) { stack.Get(context.FrameBase + a) = stack.AsSpan()[^1]; @@ -1248,7 +1248,7 @@ await ExecuteBinaryOperationMetaMethod( ); stack.PopUntil(top - 1); total--; - } while ((total = ConcatStrings(stack, total)) > 1); + } while ((total = ConcatStrings(stack, total, context.State.Platform.NumberFormat)) > 1); stack.Get(target) = stack.AsSpan()[^1]; @@ -1273,7 +1273,7 @@ CancellationToken ct stack.Push(""); return ""; } - while ((total = ConcatStrings(stack, total)) > 1) + while ((total = ConcatStrings(stack, total, state.Platform.NumberFormat)) > 1) { var top = stack.Count; var value = await ExecuteBinaryOperationMetaMethod( @@ -1297,9 +1297,9 @@ CancellationToken ct } // Reduce the primitive suffix synchronously. A remaining pair needs __concat. - static int ConcatStrings(LuaStack stack, int total) + static int ConcatStrings(LuaStack stack, int total, LuaNumberFormat numberFormat) { - static bool ToString(ref LuaValue v) + static bool ToString(ref LuaValue v, LuaNumberFormat numberFormat) { if (v.Type == LuaValueType.String) { @@ -1308,7 +1308,7 @@ static bool ToString(ref LuaValue v) if (v.Type == LuaValueType.Number) { - v = v.ToString(); + v = v.ToLuaString(numberFormat); return true; } @@ -1321,13 +1321,16 @@ static bool ToString(ref LuaValue v) var n = 2; ref var lhs = ref stack.Get(top - 2); ref var rhs = ref stack.Get(top - 1); - if (!(lhs.Type is LuaValueType.String or LuaValueType.Number) || !ToString(ref rhs)) + if ( + !(lhs.Type is LuaValueType.String or LuaValueType.Number) + || !ToString(ref rhs, numberFormat) + ) { return total; } else if (rhs.UnsafeReadString().Length == 0) { - ToString(ref lhs); + ToString(ref lhs, numberFormat); } else if (lhs.TryReadString(out var str) && str.Length == 0) { @@ -1341,7 +1344,7 @@ static bool ToString(ref LuaValue v) for (; i < total; i++) { ref var v = ref stack.Get(top - i - 1); - if (!ToString(ref v)) + if (!ToString(ref v, numberFormat)) { break; } diff --git a/src/Lua/Standard/BasicLibrary.cs b/src/Lua/Standard/BasicLibrary.cs index 72be5d6b..5e379eaf 100644 --- a/src/Lua/Standard/BasicLibrary.cs +++ b/src/Lua/Standard/BasicLibrary.cs @@ -564,7 +564,7 @@ CancellationToken cancellationToken if (value.Type == LuaValueType.String) text = value.UnsafeReadString(); else if (value.Type == LuaValueType.Number) - text = value.ToString(); + text = value.ToLuaString(context.State.Platform.NumberFormat); else { LuaRuntimeException.BadArgument(context.State, 1, LuaValueType.String, value.Type); diff --git a/src/Lua/Standard/IOLibrary.cs b/src/Lua/Standard/IOLibrary.cs index 478d83c3..5eda6787 100644 --- a/src/Lua/Standard/IOLibrary.cs +++ b/src/Lua/Standard/IOLibrary.cs @@ -95,7 +95,9 @@ CancellationToken cancellationToken else { var stream = await context.GlobalState.Platform.FileSystem.Open( - context.State.Platform.DecodeText(arg.ToString()), + context.State.Platform.DecodeText( + arg.ToLuaString(context.State.Platform.NumberFormat) + ), LuaFileOpenMode.Read, cancellationToken ); @@ -263,7 +265,9 @@ CancellationToken cancellationToken } var stream = await context.GlobalState.Platform.FileSystem.Open( - context.State.Platform.DecodeText(arg.ToString()), + context.State.Platform.DecodeText( + arg.ToLuaString(context.State.Platform.NumberFormat) + ), LuaFileOpenMode.Write, cancellationToken ); diff --git a/src/Lua/Standard/Internal/IOHelper.cs b/src/Lua/Standard/Internal/IOHelper.cs index af51ed20..4f55a61f 100644 --- a/src/Lua/Standard/Internal/IOHelper.cs +++ b/src/Lua/Standard/Internal/IOHelper.cs @@ -1,4 +1,3 @@ -using System.Globalization; using Lua.Internal; using Lua.IO; @@ -71,7 +70,12 @@ CancellationToken cancellationToken { using PooledArray fileBuffer = new(64); var span = fileBuffer.AsSpan(); - d.TryFormat(span, out var charsWritten, provider: CultureInfo.InvariantCulture); + NumberFormatter.TryFormat( + d, + span, + out var charsWritten, + context.State.Platform.NumberFormat + ); await file.WriteAsync( fileBuffer.UnderlyingArray.AsMemory(0, charsWritten), cancellationToken diff --git a/src/Lua/Standard/StringLibrary.cs b/src/Lua/Standard/StringLibrary.cs index bf2b9696..1f8a92c8 100644 --- a/src/Lua/Standard/StringLibrary.cs +++ b/src/Lua/Standard/StringLibrary.cs @@ -220,7 +220,8 @@ CancellationToken cancellationToken a, precision, alternateForm, - specifier == 'A' + specifier == 'A', + context.State.Platform.NumberFormat ); if (BitConverter.DoubleToInt64Bits(a) >= 0) { @@ -274,7 +275,13 @@ CancellationToken cancellationToken LuaValueType.Number, parameter.Type ); - formattedValue = FormatFloat(f, specifier, precision, alternateForm); + formattedValue = FormatFloat( + f, + specifier, + precision, + alternateForm, + context.State.Platform.NumberFormat + ); if (double.IsNaN(f) || double.IsInfinity(f)) zeroPadding = false; if (BitConverter.DoubleToInt64Bits(f) >= 0) @@ -302,7 +309,9 @@ CancellationToken cancellationToken { case LuaValueType.String: case LuaValueType.Number: - formattedValue = StringHelper.Quote(parameter.ToString()); + formattedValue = StringHelper.Quote( + parameter.ToLuaString(context.State.Platform.NumberFormat) + ); break; default: LuaRuntimeException.BadArgument( @@ -609,7 +618,7 @@ or LuaValueType.Function var replacementText = repl.Type == LuaValueType.String ? repl.UnsafeReadString() : repl.Type == LuaValueType.Number - ? repl.UnsafeReadDouble().ToString(default, CultureInfo.InvariantCulture) + ? repl.ToLuaString(context.State.Platform.NumberFormat) : null; var replacedBuilder = replacementText != null && replacementText.IndexOf('%') >= 0 @@ -707,7 +716,7 @@ or LuaValueType.Function } else if (result.TryRead(out var rd)) { - AppendNumber(builder, rd); + AppendNumber(builder, rd, context.State.Platform.NumberFormat); } else if (!result.ToBoolean()) { @@ -1032,19 +1041,24 @@ static int ReadStringIndex(in LuaFunctionExecutionContext context, int index) #endif } - static void AppendNumber(StringBuilder builder, double value) + static void AppendNumber(StringBuilder builder, double value, LuaNumberFormat format) { Span buffer = stackalloc char[32]; - value.TryFormat(buffer, out var written, default, CultureInfo.InvariantCulture); + NumberFormatter.TryFormat(value, buffer, out var written, format); builder.Append(buffer[..written]); } - static string FormatFloat(double value, char specifier, int precision, bool alternateForm) + static string FormatFloat( + double value, + char specifier, + int precision, + bool alternateForm, + LuaNumberFormat numberFormat + ) { if (double.IsNaN(value) || double.IsInfinity(value)) { - var special = value.ToString(CultureInfo.InvariantCulture); - return specifier is 'E' or 'G' ? special.ToUpperInvariant() : special; + return NumberFormatter.FormatSpecial(value, numberFormat, specifier is 'E' or 'G'); } var general = specifier is 'g' or 'G'; precision = diff --git a/src/Lua/Standard/TableLibrary.cs b/src/Lua/Standard/TableLibrary.cs index 5362163d..2d30f3da 100644 --- a/src/Lua/Standard/TableLibrary.cs +++ b/src/Lua/Standard/TableLibrary.cs @@ -1,4 +1,3 @@ -using System.Globalization; using System.Runtime.CompilerServices; using Lua.Internal; using Lua.Internal.CompilerServices; @@ -103,12 +102,11 @@ CancellationToken cancellationToken { var number = value.UnsafeReadDouble(); if ( - MathEx.IsFinite(number) - && number.TryFormat( + NumberFormatter.TryFormat( + number, numberBuffer, out var written, - default, - provider: CultureInfo.InvariantCulture + context.State.Platform.NumberFormat ) ) { @@ -116,7 +114,7 @@ CancellationToken cancellationToken } else { - builder.AddRange(value.ToString()); + builder.AddRange(value.ToLuaString(context.State.Platform.NumberFormat)); } } else diff --git a/tests/Lua.Tests/NumericTextPolicyTests.cs b/tests/Lua.Tests/NumericTextPolicyTests.cs new file mode 100644 index 00000000..465b9ded --- /dev/null +++ b/tests/Lua.Tests/NumericTextPolicyTests.cs @@ -0,0 +1,248 @@ +using System.Globalization; +using System.Text; +using Lua.IO; +using Lua.Platforms; +using Lua.Standard; +using Lua.Tests.Helpers; + +namespace Lua.Tests; + +[TestFixture(LuaNumberFormat.DotNet)] +[TestFixture(LuaNumberFormat.Lua)] +public class NumericTextPolicyTests(LuaNumberFormat numberFormat) +{ + LuaState CreateState(LuaStringMode stringMode = LuaStringMode.Utf16) + { + var platform = + stringMode == LuaStringMode.Bytes + ? LuaPlatform.DefaultByteStrings + : LuaPlatform.Default; + var state = LuaState.Create(platform with { NumberFormat = numberFormat }); + state.OpenStandardLibraries(); + return state; + } + + string Expected(double value, string luaText) => + numberFormat == LuaNumberFormat.Lua + ? luaText + : value.ToString(CultureInfo.InvariantCulture); + + [TestCase(0d, "0")] + [TestCase(-0d, "-0")] + [TestCase(1d / 3, "0.33333333333333")] + [TestCase(1e-4, "0.0001")] + [TestCase(1e-5, "1e-05")] + [TestCase(1e13, "10000000000000")] + [TestCase(1e14, "1e+14")] + [TestCase(99999999999999.5, "1e+14")] + [TestCase(double.Epsilon, "4.9406564584125e-324")] + [TestCase(double.MaxValue, "1.7976931348623e+308")] + [TestCase(double.PositiveInfinity, "inf")] + [TestCase(double.NegativeInfinity, "-inf")] + public async Task ImplicitConversionsShareThePlatformFormat(double value, string luaText) + { + using var state = CreateState(); + state.Environment["number"] = value; + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR"); + var results = await state.DoStringAsync( + """ + local object=setmetatable({}, {__tostring=function() return number end}) + return tostring(number), number..'', table.concat({number}), + string.format('%s',number), tostring(object), + (string.gsub('x','x',number)), + (string.gsub('x','x',function() return number end)), + (string.gsub('x','x',{x=number})) + """ + ); + Assert.That( + results.Select(v => v.Read()), + Is.All.EqualTo(Expected(value, luaText)) + ); + Assert.That( + new LuaValue(value).ToString(), + Is.EqualTo(value.ToString(CultureInfo.InvariantCulture)) + ); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } + + [TestCase(false, false)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(true, true)] + public async Task NaNTextPreservesSignWithoutCrtPayloadSuffixes(bool negative, bool signaling) + { + var bits = signaling ? 0x7ff0000000000001UL : 0x7ff8000000000000UL; + if (negative) + bits |= 1UL << 63; + var value = BitConverter.Int64BitsToDouble(unchecked((long)bits)); + using var state = CreateState(); + state.Environment["number"] = value; + var results = await state.DoStringAsync( + "return tostring(number),number..'',table.concat({number}),string.format('%q',number)" + ); + var text = Expected(value, negative ? "-nan" : "nan"); + Assert.That(results.Take(3).Select(v => v.Read()), Is.All.EqualTo(text)); + Assert.That(results[3].Read(), Is.EqualTo('"' + text + '"')); + Assert.That( + BitConverter.DoubleToInt64Bits(state.Environment["number"].Read()), + Is.EqualTo(unchecked((long)bits)) + ); + } + + [Test] + public async Task QuotesAndExplicitBaseConversionUseImplicitNumericText() + { + using var state = CreateState(); + var result = await state.DoStringAsync( + """ + return string.format('%+.2q',1/3), tonumber(1e14,10), + tonumber('100000000000000',10), tonumber('1e14'), + tonumber('inf'), tonumber('nan'), tonumber(123,16) + """ + ); + var expectedNumber = + numberFormat == LuaNumberFormat.Lua ? LuaValue.Nil : new LuaValue(1e14); + Assert.That( + result, + Is.EqualTo( + new LuaValue[] + { + '"' + Expected(1d / 3, "0.33333333333333") + '"', + expectedNumber, + 1e14, + 1e14, + LuaValue.Nil, + LuaValue.Nil, + 291, + } + ) + ); + } + + [Test] + public async Task ExplicitFiniteFormatsKeepTheirRequestedPrecision() + { + using var state = CreateState(); + var result = await state.DoStringAsync( + "return string.format('%f|%e|%g|%.17g|%#.4g|%.2a|%A',1/3,1/3,1/3,1/3,1,1/3,1)" + ); + Assert.That( + result[0].Read(), + Is.EqualTo("0.333333|3.333333e-01|0.333333|0.33333333333333331|1.000|0x1.55p-2|0X1P+0") + ); + } + + [TestCase("f")] + [TestCase("e")] + [TestCase("E")] + [TestCase("g")] + [TestCase("G")] + [TestCase("a")] + [TestCase("A")] + public async Task ExplicitSpecialFormatsHonorSignCaseAndPadding(string specifier) + { + using var state = CreateState(); + state.Environment["negativeNaN"] = BitConverter.Int64BitsToDouble( + unchecked((long)0xfff8000000000000UL) + ); + state.Environment["positiveNaN"] = BitConverter.Int64BitsToDouble(0x7ff8000000000000); + var result = await state.DoStringAsync( + $"return string.format('%+08{specifier}|%08{specifier}|%08{specifier}|%+08{specifier}',math.huge,-math.huge,negativeNaN,positiveNaN)" + ); + var upper = specifier is "E" or "G" or "A"; + var infinity = numberFormat == LuaNumberFormat.Lua ? "inf" : "Infinity"; + var nan = numberFormat == LuaNumberFormat.Lua ? "nan" : "NaN"; + if (upper) + { + infinity = infinity.ToUpperInvariant(); + nan = nan.ToUpperInvariant(); + } + var negativeNan = numberFormat == LuaNumberFormat.Lua ? "-" + nan : nan; + var expected = string.Join( + '|', + ("+" + infinity).PadLeft(8), + ("-" + infinity).PadLeft(8), + negativeNan.PadLeft(8), + ("+" + nan).PadLeft(8) + ); + Assert.That(result[0].Read(), Is.EqualTo(expected)); + } + + [TestCase(LuaStringMode.Utf16)] + [TestCase(LuaStringMode.Bytes)] + public async Task FileAndDefaultOutputUseThePlatformFormat(LuaStringMode stringMode) + { + using var state = CreateState(stringMode); + using var buffer = new MemoryStream(); + using var stream = new LuaStream(LuaFileOpenMode.Write, buffer, stringMode); + state.Environment["file"] = new LuaValue(new FileHandle(stream)); + await state.DoStringAsync( + "file:write(1/3,'|',math.huge);io.output(file);io.write('|',1/3)" + ); + var text = Expected(1d / 3, "0.33333333333333"); + Assert.That( + Encoding.UTF8.GetString(buffer.ToArray()), + Is.EqualTo(text + '|' + Expected(double.PositiveInfinity, "inf") + '|' + text) + ); + } + + [Test] + public async Task NumericInputAndOutputFilenamesUseThePlatformFormat() + { + var files = new RecordingFileSystem(); + using var state = LuaState.Create( + LuaPlatform.Default with + { + NumberFormat = numberFormat, + FileSystem = files, + } + ); + state.OpenStandardLibraries(); + await state.DoStringAsync("io.input(1/3):close();io.output(1/3):close()"); + Assert.That( + files.Paths, + Is.EqualTo( + new[] { Expected(1d / 3, "0.33333333333333"), Expected(1d / 3, "0.33333333333333") } + ) + ); + } + + sealed class RecordingFileSystem : NotImplementedExceptionFileSystemBase + { + public readonly List Paths = new(); + + public override ValueTask Open( + string path, + LuaFileOpenMode mode, + CancellationToken cancellationToken + ) + { + Paths.Add(path); + return new(ILuaStream.CreateFromStream(new MemoryStream(), mode)); + } + } + + [Test] + public async Task CoroutinesInheritTheFormatAndOtherStatesRemainIndependent() + { + using var state = CreateState(); + using var other = LuaState.Create(); + other.OpenStandardLibraries(); + var result = await state.DoStringAsync( + "return coroutine.wrap(function() return tostring(1/3) end)()" + ); + Assert.That(result[0].Read(), Is.EqualTo(Expected(1d / 3, "0.33333333333333"))); + Assert.That( + (await other.DoStringAsync("return tostring(1/3)"))[0].Read(), + Is.EqualTo((1d / 3).ToString(CultureInfo.InvariantCulture)) + ); + Assert.That(LuaPlatform.Default.NumberFormat, Is.EqualTo(LuaNumberFormat.DotNet)); + } +}