diff --git a/Jint.Tests/Runtime/EngineTests.cs b/Jint.Tests/Runtime/EngineTests.cs index 86f7e6c7b..31b6b19d7 100644 --- a/Jint.Tests/Runtime/EngineTests.cs +++ b/Jint.Tests/Runtime/EngineTests.cs @@ -811,6 +811,41 @@ public void ShouldWriteNumbersUsingBases() "); } + [Fact] + public void ShouldWriteLargeNumbersUsingBasesWithoutOverflow() + { + // Values above long.MaxValue (~9.22e18) previously overflowed when cast to long, + // producing wrong results or runtime errors. The expected strings below are the + // mathematically exact base-r representation of the stored double value. + RunTest(@" + // Power-of-2 radices: bit-exact, matches V8/SpiderMonkey output. + assert((-12345e+30).toString(2) === '-100110000010100111110011100101010111110111100101011010000000000000000000000000000000000000000000000000000000000000'); + assert((-12345e+30).toString(16) === '-260a7ce55f795a000000000000000'); + assert((1e+20).toString(16) === '56bc75e2d63100000'); + assert((1e+20).toString(8) === '12657072742654304000000'); + + // Non-power-of-2 radices: previously gave wrong digits (or threw). + // Jint emits the mathematically exact representation of the stored double, + // which differs from V8's approximation for these values - both are valid + // per the 'implementation-approximated' clause of the spec. + assert((1e+20).toString(3) === '220200020122120112010222022122002000100201'); + assert((1e+20).toString(7) === '344015313561621001452562'); + assert((1e+25).toString(36) === '198exbvshgq9n8mps'); + assert((-12345e+30).toString(5) === '-3214133420230123110000004012030333321240102013132'); + + // Small values must still go through the fast (long) path. + assert((255).toString(16) === 'ff'); + assert((-255).toString(16) === '-ff'); + assert((0).toString(2) === '0'); + assert((1).toString(2) === '1'); + assert((15.1).toString(36) === 'f.3llllllllkau6snqkpygsc3di'); + + // Around the long.MaxValue boundary (2^63 = 9223372036854775808 as double). + assert((9223372036854775000).toString(16) === '7ffffffffffffc00'); + assert((-9223372036854775000).toString(16) === '-7ffffffffffffc00'); + "); + } + [Fact] public void ShouldNotAlterSlashesInRegex() { diff --git a/Jint/Native/Number/NumberPrototype.cs b/Jint/Native/Number/NumberPrototype.cs index 8410d32a6..543986a0d 100644 --- a/Jint/Native/Number/NumberPrototype.cs +++ b/Jint/Native/Number/NumberPrototype.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Globalization; +using System.Numerics; using System.Text; using Jint.Native.Number.Dtoa; using Jint.Native.Object; @@ -446,13 +447,27 @@ private JsValue ToNumberString(JsValue thisObject, JsCallArguments arguments) return ToNumberString(x); } - var integer = (long) x; - var fraction = x - integer; + var truncated = System.Math.Truncate(x); + var fraction = x - truncated; + + string result; + // (double) long.MaxValue rounds up to 2^63, so the comparison must be strict + // against 2^63 (the smallest double above long.MaxValue) to keep the cast safe. + if (truncated < 9223372036854775808.0) + { + result = ToBase((long) truncated, radix); + } + else + { + // For values that don't fit in long, use BigInteger to preserve the exact + // integer represented by the double. Doubles above 2^53 have no fractional + // part, but the integer can be up to ~2^1024. + result = ToBase(new BigInteger(truncated), radix); + } - string result = NumberPrototype.ToBase(integer, radix); if (fraction != 0) { - result += "." + NumberPrototype.ToFractionBase(fraction, radix); + result += "." + ToFractionBase(fraction, radix); } return result; @@ -490,6 +505,26 @@ private static string ToBaseCore(long n, int radix) return sb.ToString(); } + internal static string ToBase(BigInteger n, int radix) + { + if (n.IsZero) + { + return "0"; + } + + const string Digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + // Doubles can represent integers up to ~2^1024, which is ~1024 binary digits. + var sb = new ValueStringBuilder(stackalloc char[1100]); + var radixBig = new BigInteger(radix); + while (n > 0) + { + n = BigInteger.DivRem(n, radixBig, out var remainder); + sb.Append(Digits[(int) remainder]); + } + sb.Reverse(); + return sb.ToString(); + } + internal static string ToFractionBase(double n, int radix) { // based on the repeated multiplication method