Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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することでエラー時の処理を行うことができます。
Expand Down
5 changes: 3 additions & 2 deletions src/Lua/Internal/HexConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
53 changes: 53 additions & 0 deletions src/Lua/Internal/NumberFormatter.cs
Original file line number Diff line number Diff line change
@@ -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<char> 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;
}
}
11 changes: 11 additions & 0 deletions src/Lua/LuaNumberFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Lua;

/// <summary>The numeric text representation used by Lua execution.</summary>
public enum LuaNumberFormat
{
/// <summary>Invariant .NET formatting. This is the default.</summary>
DotNet,

/// <summary>Lua 5.2's default C-locale 14 significant digits and portable signed inf/nan spellings.</summary>
Lua,
}
9 changes: 7 additions & 2 deletions src/Lua/LuaValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -766,7 +769,7 @@ CancellationToken cancellationToken
}
else
{
context.State.Stack.Push(ToString());
context.State.Stack.Push(ToLuaString(context.State.Platform.NumberFormat));
return new(1);
}

Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/Lua/Platforms/LuaPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ TimeProvider TimeProvider
/// </summary>
public LuaStringMode StringMode { get; init; }

/// <summary>
/// Controls numeric text produced by Lua execution, including implicit conversions and I/O.
/// Defaults to <see cref="LuaNumberFormat.DotNet"/>. Explicit floating-point formats retain
/// their requested precision; this option selects their infinity and NaN spellings.
/// Does not change <see cref="LuaValue.ToString()"/>, arithmetic or parsing of numeric strings.
/// </summary>
public LuaNumberFormat NumberFormat { get; init; }

/// <summary>
/// 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.
Expand Down
21 changes: 12 additions & 9 deletions src/Lua/Runtime/LuaVirtualMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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];

Expand All @@ -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(
Expand All @@ -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)
{
Expand All @@ -1308,7 +1308,7 @@ static bool ToString(ref LuaValue v)

if (v.Type == LuaValueType.Number)
{
v = v.ToString();
v = v.ToLuaString(numberFormat);
return true;
}

Expand All @@ -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)
{
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Lua/Standard/BasicLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions src/Lua/Standard/IOLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down Expand Up @@ -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
);
Expand Down
8 changes: 6 additions & 2 deletions src/Lua/Standard/Internal/IOHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Globalization;
using Lua.Internal;
using Lua.IO;

Expand Down Expand Up @@ -71,7 +70,12 @@ CancellationToken cancellationToken
{
using PooledArray<char> 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
Expand Down
34 changes: 24 additions & 10 deletions src/Lua/Standard/StringLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ CancellationToken cancellationToken
a,
precision,
alternateForm,
specifier == 'A'
specifier == 'A',
context.State.Platform.NumberFormat
);
if (BitConverter.DoubleToInt64Bits(a) >= 0)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -707,7 +716,7 @@ or LuaValueType.Function
}
else if (result.TryRead<double>(out var rd))
{
AppendNumber(builder, rd);
AppendNumber(builder, rd, context.State.Platform.NumberFormat);
}
else if (!result.ToBoolean())
{
Expand Down Expand Up @@ -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<char> 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 =
Expand Down
Loading
Loading