diff --git a/src/MoonSharp.Interpreter.Tests/EndToEnd/TonumberTests.cs b/src/MoonSharp.Interpreter.Tests/EndToEnd/TonumberTests.cs
new file mode 100644
index 00000000..bc5187b0
--- /dev/null
+++ b/src/MoonSharp.Interpreter.Tests/EndToEnd/TonumberTests.cs
@@ -0,0 +1,119 @@
+using NUnit.Framework;
+
+namespace MoonSharp.Interpreter.Tests.EndToEnd
+{
+ ///
+ /// Fork tests for tonumber(e, base). The upstream implementation delegated to
+ /// Convert.ToUInt32, which let raw CLR FormatException/OverflowException escape the
+ /// interpreter on unparseable or oversized input instead of returning nil per Lua
+ /// semantics — a host-crash primitive when tonumber is fed untrusted strings.
+ ///
+ [TestFixture]
+ public class TonumberTests
+ {
+ private static DynValue Run(string code) { return Script.RunString(code); }
+ private static double Num(string code) { return Run(code).Number; }
+ private static bool IsNil(string code) { return Run(code).IsNil(); }
+
+ [Test]
+ public void Tonumber_WithBase_NullBackedStringReturnsNilNotCrash()
+ {
+ // A host can hand the interpreter a string DynValue wrapping a null
+ // (DynValue.NewString(null)). Feeding it to tonumber(x, base) must yield nil,
+ // not a raw NullReferenceException escaping into the host.
+ Script s = new Script();
+ s.Globals.Set("x", DynValue.NewString((string)null));
+ DynValue r = s.DoString("return tonumber(x, 16)");
+ Assert.IsTrue(r.IsNil());
+ }
+
+ [Test]
+ public void Tonumber_WithBase_ParsesValidNumerals()
+ {
+ Assert.AreEqual(255.0, Num("return tonumber('ff', 16)"));
+ Assert.AreEqual(255.0, Num("return tonumber('FF', 16)"));
+ Assert.AreEqual(2.0, Num("return tonumber('10', 2)"));
+ Assert.AreEqual(511.0, Num("return tonumber('777', 8)"));
+ Assert.AreEqual(123.0, Num("return tonumber('123', 10)"));
+ Assert.AreEqual(15.0, Num("return tonumber('120', 3)"));
+ Assert.AreEqual(71.0, Num("return tonumber('78', 9)"));
+ }
+
+ [Test]
+ public void Tonumber_WithBase_SupportsBasesUpTo36()
+ {
+ Assert.AreEqual(35.0, Num("return tonumber('z', 36)"));
+ Assert.AreEqual(1295.0, Num("return tonumber('zz', 36)"));
+ Assert.AreEqual(19.0, Num("return tonumber('j', 20)"));
+ }
+
+ [Test]
+ public void Tonumber_WithBase_HandlesSignAndWhitespace()
+ {
+ Assert.AreEqual(-255.0, Num("return tonumber('-ff', 16)"));
+ Assert.AreEqual(16.0, Num("return tonumber(' 10 ', 16)"));
+ Assert.AreEqual(-5.0, Num("return tonumber(' -101 ', 2)"));
+ }
+
+ [Test]
+ public void Tonumber_WithBase_CoercesNumberFirstArgument()
+ {
+ Assert.AreEqual(5.0, Num("return tonumber(101, 2)"));
+ }
+
+ [Test]
+ public void Tonumber_WithBase_ReturnsNilOnUnparseableInput()
+ {
+ // These previously escaped as a raw .NET FormatException.
+ Assert.IsTrue(IsNil("return tonumber('zz', 16)"));
+ Assert.IsTrue(IsNil("return tonumber('not hex!', 16)"));
+ Assert.IsTrue(IsNil("return tonumber('0x10', 16)")); // '0x' prefix is not a hex digit in Lua
+ Assert.IsTrue(IsNil("return tonumber('12.5', 10)"));
+ Assert.IsTrue(IsNil("return tonumber('1 2', 16)"));
+ Assert.IsTrue(IsNil("return tonumber('', 16)"));
+ Assert.IsTrue(IsNil("return tonumber(' ', 16)"));
+ Assert.IsTrue(IsNil("return tonumber('-', 16)"));
+ Assert.IsTrue(IsNil("return tonumber('+10', 16)")); // only '-' is a valid sign in Lua
+ }
+
+ [Test]
+ public void Tonumber_WithBase_ReturnsNilOnDigitOutOfBase()
+ {
+ // Previously raised ScriptRuntimeException 'invalid character' for bases 3-9.
+ Assert.IsTrue(IsNil("return tonumber('2', 2)"));
+ Assert.IsTrue(IsNil("return tonumber('9', 8)"));
+ Assert.IsTrue(IsNil("return tonumber('17', 6)"));
+ Assert.IsTrue(IsNil("return tonumber('ff', 10)"));
+ }
+
+ [Test]
+ public void Tonumber_WithBase_LargeValuesDoNotThrow()
+ {
+ // Previously escaped as a raw .NET OverflowException (> uint.MaxValue).
+ DynValue v = Run("return tonumber(string.rep('f', 20), 16)");
+ Assert.AreEqual(DataType.Number, v.Type);
+ Assert.Greater(v.Number, 1e23);
+ }
+
+ [Test]
+ public void Tonumber_WithBase_OutOfRangeBaseStillRaises()
+ {
+ Assert.Throws(() => Run("return tonumber('10', 1)"));
+ Assert.Throws(() => Run("return tonumber('10', 37)"));
+ Assert.Throws(() => Run("return tonumber('10', 200)"));
+ }
+
+ [Test]
+ public void Tonumber_WithBase_BadInputReturnsNilNotCrash()
+ {
+ // A script must be able to branch on bad input (nil) rather than have the
+ // call escape as a CLR exception into the embedding host.
+ DynValue v = Run(@"
+ local ok = tonumber('deadbeef', 16)
+ local bad = tonumber('DEAD BEEF QUOTE', 16)
+ return ok ~= nil and bad == nil
+ ");
+ Assert.IsTrue(v.Boolean);
+ }
+ }
+}
diff --git a/src/MoonSharp.Interpreter.Tests/TestMore/301-basic.t b/src/MoonSharp.Interpreter.Tests/TestMore/301-basic.t
index 62eb0970..ffe73f81 100644
--- a/src/MoonSharp.Interpreter.Tests/TestMore/301-basic.t
+++ b/src/MoonSharp.Interpreter.Tests/TestMore/301-basic.t
@@ -327,9 +327,7 @@ error_like(function () tonumber('111', 200) end,
"^[^:]+:%d+: bad argument #2 to 'tonumber' %(base out of range%)",
"function tonumber (bad base)")
-error_like(function () tonumber('17', 6) end,
- "^[^:]+:%d+: bad argument #1 to 'tonumber' %(invalid character%)",
- "function tonumber (bad base)")
+is(tonumber('17', 6), nil, "function tonumber (invalid digit for base)")
is(tostring('text'), 'text', "function tostring")
is(tostring(3.14), '3.14')
diff --git a/src/MoonSharp.Interpreter/CoreLib/BasicModule.cs b/src/MoonSharp.Interpreter/CoreLib/BasicModule.cs
index 5ed5b277..e87331ba 100755
--- a/src/MoonSharp.Interpreter/CoreLib/BasicModule.cs
+++ b/src/MoonSharp.Interpreter/CoreLib/BasicModule.cs
@@ -238,44 +238,71 @@ public static DynValue tonumber(ScriptExecutionContext executionContext, Callbac
}
else
{
- //!COMPAT: tonumber supports only 2,8,10 or 16 as base
- //UPDATE: added support for 3-9 base numbers
- DynValue ee;
+ DynValue ee;
if (args[0].Type != DataType.Number)
ee = args.AsType(0, "tonumber", DataType.String, false);
else
- ee = DynValue.NewString(args[0].Number.ToString(CultureInfo.InvariantCulture)); ;
+ ee = DynValue.NewString(args[0].Number.ToString(CultureInfo.InvariantCulture));
int bb = (int)b.Number;
- uint uiv = 0;
- if (bb == 2 || bb == 8 || bb == 10 || bb == 16)
- {
- uiv = Convert.ToUInt32(ee.String.Trim(), bb);
- }
- else if (bb < 10 && bb > 2) // Support for 3, 4, 5, 6, 7 and 9 based numbers
- {
- foreach (char digit in ee.String.Trim())
- {
- int value = digit - 48;
- if (value < 0 || value >= bb)
- {
- throw new ScriptRuntimeException("bad argument #1 to 'tonumber' (invalid character)");
- }
-
- uiv = (uint)(uiv * bb) + (uint)value;
- }
- }
- else
- {
- throw new ScriptRuntimeException("bad argument #2 to 'tonumber' (base out of range)");
- }
-
- return DynValue.NewNumber(uiv);
+ if (bb < 2 || bb > 36)
+ throw new ScriptRuntimeException("bad argument #2 to 'tonumber' (base out of range)");
+
+ return StringToNumberInBase(ee.String, bb);
}
}
+ // Integer numeral parsing for tonumber(e, base), following standard Lua semantics:
+ // optional surrounding whitespace, optional '-' sign, then one or more digits
+ // (0-9, a-z/A-Z for 10-35) all below the base. Any other input yields nil — never
+ // a CLR exception, which would escape the interpreter as a non-Lua error.
+ private static DynValue StringToNumberInBase(string str, int numBase)
+ {
+ // A string DynValue can wrap a null (e.g. host-created DynValue.NewString(null));
+ // treat it as unparseable rather than dereferencing it into a NullReferenceException.
+ if (str == null)
+ return DynValue.Nil;
+
+ string s = str.Trim();
+ int i = 0;
+ bool neg = false;
+
+ if (i < s.Length && s[i] == '-')
+ {
+ neg = true;
+ i++;
+ }
+
+ if (i >= s.Length)
+ return DynValue.Nil;
+
+ double value = 0.0;
+
+ for (; i < s.Length; i++)
+ {
+ char c = s[i];
+ int digit;
+
+ if (c >= '0' && c <= '9')
+ digit = c - '0';
+ else if (c >= 'a' && c <= 'z')
+ digit = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'Z')
+ digit = c - 'A' + 10;
+ else
+ return DynValue.Nil;
+
+ if (digit >= numBase)
+ return DynValue.Nil;
+
+ value = value * numBase + digit;
+ }
+
+ return DynValue.NewNumber(neg ? -value : value);
+ }
+
[MoonSharpModuleMethod]
public static DynValue print(ScriptExecutionContext executionContext, CallbackArguments args)
{