Skip to content

Commit f7bd5df

Browse files
committed
fix: align numeric coercion and math selection with Lua 5.2
1 parent 737ab3f commit f7bd5df

11 files changed

Lines changed: 461 additions & 206 deletions

‎src/Lua/LuaValue.cs‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -361,13 +361,18 @@ bool TryParseToDouble(out double result)
361361
}
362362

363363
var str = Unsafe.As<string>(referenceValue!);
364-
var span = str.AsSpan().Trim();
365-
if (span.Length == 0)
364+
var span = str.AsSpan();
365+
if (!span.IsEmpty && (span[0] <= ' ' || span[^1] <= ' '))
366+
span = span.Trim(" \f\n\r\t\v");
367+
// .NET's parser accepts trailing NULs, Unicode whitespace and named
368+
// infinities/NaNs. A Lua numeral must end with a hex digit or decimal point.
369+
if (span.Length == 0 || !(StringHelper.IsDigit(span[^1]) || span[^1] == '.'))
366370
{
367371
result = default!;
368372
return false;
369373
}
370374

375+
var numberText = span;
371376
var sign = 1;
372377
var first = span[0];
373378
if (first is '+')
@@ -381,6 +386,12 @@ bool TryParseToDouble(out double result)
381386
span = span[1..];
382387
}
383388

389+
if (span.IsEmpty || !(span[0] is >= '0' and <= '9' or '.'))
390+
{
391+
result = default;
392+
return false;
393+
}
394+
384395
if (span.Length > 2 && span[0] is '0' && span[1] is 'x' or 'X')
385396
{
386397
// TODO: optimize
@@ -399,7 +410,7 @@ bool TryParseToDouble(out double result)
399410
else
400411
{
401412
return double.TryParse(
402-
str,
413+
numberText,
403414
NumberStyles.Float,
404415
CultureInfo.InvariantCulture,
405416
out result

‎src/Lua/Standard/BasicLibrary.cs‎

Lines changed: 64 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -491,34 +491,24 @@ public ValueTask<int> Select(
491491
CancellationToken cancellationToken
492492
)
493493
{
494-
var arg0 = context.GetArgument(0);
495-
496-
if (arg0.TryRead<int>(out var index))
497-
{
498-
if (Math.Abs(index) > context.ArgumentCount)
499-
{
500-
throw new LuaRuntimeException(
501-
context.State,
502-
"bad argument #1 to 'select' (index out of range)"
503-
);
504-
}
505-
506-
var span =
507-
index >= 0
508-
? context.Arguments[index..]
509-
: context.Arguments[(context.ArgumentCount + index)..];
510-
511-
return new(context.Return(span));
512-
}
513-
else if (arg0.TryRead<string>(out var str) && str == "#")
514-
{
515-
return new(context.Return(context.ArgumentCount - 1));
516-
}
517-
else
518-
{
519-
LuaRuntimeException.BadArgument(context.State, 1, LuaValueType.Number, arg0.Type);
520-
return default;
521-
}
494+
var selector = context.GetArgument(0);
495+
if (selector.Type == LuaValueType.String)
496+
{
497+
var text = selector.UnsafeReadString();
498+
if (text.Length > 0 && text[0] == '#')
499+
return new(context.Return(context.ArgumentCount - 1));
500+
}
501+
502+
var index = Math.Truncate(context.GetArgument<double>(0));
503+
if (!MathEx.IsFinite(index))
504+
context.ThrowBadArgument(1, "index out of range");
505+
if (index < 0)
506+
index += context.ArgumentCount;
507+
else if (index > context.ArgumentCount)
508+
index = context.ArgumentCount;
509+
if (index < 1)
510+
context.ThrowBadArgument(1, "index out of range");
511+
return new(context.Return(context.Arguments[(int)index..]));
522512
}
523513

524514
public ValueTask<int> SetMetatable(
@@ -560,152 +550,65 @@ public ValueTask<int> ToNumber(
560550
CancellationToken cancellationToken
561551
)
562552
{
563-
var e = context.GetArgument(0);
564-
int? toBase = context.HasArgument(1) ? (int)context.GetArgument<double>(1) : null;
565-
566-
if (toBase != null && (toBase < 2 || toBase > 36))
567-
{
568-
throw new LuaRuntimeException(
569-
context.State,
570-
"bad argument #2 to 'tonumber' (base out of range)"
553+
var value = context.GetArgument(0);
554+
if (!context.HasArgument(1))
555+
{
556+
if (value.Type == LuaValueType.Number)
557+
return new(context.Return(value));
558+
return new(
559+
context.Return(
560+
value.TryReadDouble(out var number) && !double.IsNaN(number)
561+
? new LuaValue(number)
562+
: LuaValue.Nil
563+
)
571564
);
572565
}
573566

574-
double? value = null;
575-
if (e.Type is LuaValueType.Number)
576-
{
577-
value = e.UnsafeRead<double>();
578-
}
579-
else if (e.TryRead<string>(out var str))
580-
{
581-
if (toBase == null)
582-
{
583-
if (e.TryRead<double>(out var result))
584-
{
585-
value = result;
586-
}
587-
}
588-
else if (toBase == 10)
589-
{
590-
if (
591-
double.TryParse(
592-
str,
593-
NumberStyles.Float,
594-
CultureInfo.InvariantCulture,
595-
out var result
596-
)
597-
)
598-
{
599-
value = result;
600-
}
601-
}
602-
else
603-
{
604-
try
605-
{
606-
// if the base is not 10, str cannot contain a minus sign
607-
var span = str.AsSpan().Trim();
608-
if (span.Length == 0)
609-
{
610-
goto END;
611-
}
612-
613-
var first = span[0];
614-
var sign = first == '-' ? -1 : 1;
615-
if (first is '+' or '-')
616-
{
617-
span = span[1..];
618-
}
619-
620-
if (span.Length == 0)
621-
{
622-
goto END;
623-
}
624-
625-
if (toBase == 16 && span.Length > 2 && span[0] is '0' && span[1] is 'x' or 'X')
626-
{
627-
value = sign * HexConverter.ToDouble(span);
628-
}
629-
else
630-
{
631-
value = sign * StringToDouble(span, toBase.Value);
632-
}
633-
}
634-
catch (FormatException)
635-
{
636-
goto END;
637-
}
638-
}
639-
}
567+
// Lua coerces numeric inputs to text before interpreting an explicit base.
568+
string text;
569+
if (value.Type == LuaValueType.String)
570+
text = value.UnsafeReadString();
571+
else if (value.Type == LuaValueType.Number)
572+
text = value.ToString();
640573
else
641574
{
642-
goto END;
643-
}
644-
645-
END:
646-
if (value is double.NaN)
647-
{
648-
value = null;
575+
LuaRuntimeException.BadArgument(context.State, 1, LuaValueType.String, value.Type);
576+
return default;
649577
}
650578

651-
return new(context.Return(value ?? LuaValue.Nil));
579+
var toBase = (int)context.GetArgument<double>(1);
580+
if (toBase is < 2 or > 36)
581+
context.ThrowBadArgument(2, "base out of range");
582+
var parsed = StringToDouble(text.AsSpan(), toBase);
583+
return new(context.Return(parsed.HasValue ? new LuaValue(parsed.Value) : LuaValue.Nil));
652584
}
653585

654-
static double StringToDouble(ReadOnlySpan<char> text, int toBase)
586+
static double? StringToDouble(ReadOnlySpan<char> text, int toBase)
655587
{
656-
var value = 0.0;
657-
for (var i = 0; i < text.Length; i++)
658-
{
659-
var v = text[i] switch
588+
text = text.Trim(" \f\n\r\t\v");
589+
if (text.IsEmpty)
590+
return null;
591+
var negative = text[0] == '-';
592+
if (text[0] is '+' or '-')
593+
text = text[1..];
594+
if (text.IsEmpty)
595+
return null;
596+
597+
double value = 0;
598+
foreach (var c in text)
599+
{
600+
var digit = c switch
660601
{
661-
'0' => 0,
662-
'1' => 1,
663-
'2' => 2,
664-
'3' => 3,
665-
'4' => 4,
666-
'5' => 5,
667-
'6' => 6,
668-
'7' => 7,
669-
'8' => 8,
670-
'9' => 9,
671-
'a' or 'A' => 10,
672-
'b' or 'B' => 11,
673-
'c' or 'C' => 12,
674-
'd' or 'D' => 13,
675-
'e' or 'E' => 14,
676-
'f' or 'F' => 15,
677-
'g' or 'G' => 16,
678-
'h' or 'H' => 17,
679-
'i' or 'I' => 18,
680-
'j' or 'J' => 19,
681-
'k' or 'K' => 20,
682-
'l' or 'L' => 21,
683-
'm' or 'M' => 22,
684-
'n' or 'N' => 23,
685-
'o' or 'O' => 24,
686-
'p' or 'P' => 25,
687-
'q' or 'Q' => 26,
688-
'r' or 'R' => 27,
689-
's' or 'S' => 28,
690-
't' or 'T' => 29,
691-
'u' or 'U' => 30,
692-
'v' or 'V' => 31,
693-
'w' or 'W' => 32,
694-
'x' or 'X' => 33,
695-
'y' or 'Y' => 34,
696-
'z' or 'Z' => 35,
697-
_ => throw new FormatException("Invalid digit."),
602+
>= '0' and <= '9' => c - '0',
603+
>= 'A' and <= 'Z' => c - 'A' + 10,
604+
>= 'a' and <= 'z' => c - 'a' + 10,
605+
_ => 36,
698606
};
699-
700-
if (v >= toBase)
701-
{
702-
throw new FormatException();
703-
}
704-
705-
value += v * Math.Pow(toBase, text.Length - i - 1);
607+
if (digit >= toBase)
608+
return null;
609+
value = value * toBase + digit;
706610
}
707-
708-
return value;
611+
return negative ? -value : value;
709612
}
710613

711614
public ValueTask<int> ToString(

0 commit comments

Comments
 (0)