|
| 1 | +// Copyright 2026 International Digital Economy Academy |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +///| |
| 16 | +#callsite(autofill(loc)) |
| 17 | +fn decode(json : Json, loc~ : SourceLoc) -> @bigint.BigInt raise Failure { |
| 18 | + @json.from_json(json) catch { |
| 19 | + err => fail("unexpected decode failure: \{err}", loc~) |
| 20 | + } |
| 21 | +} |
| 22 | + |
| 23 | +///| |
| 24 | +#callsite(autofill(loc)) |
| 25 | +fn decode_error(json : Json, loc~ : SourceLoc) -> String raise Failure { |
| 26 | + let err = try ignore((@json.from_json(json) : @bigint.BigInt)) catch { |
| 27 | + err => err |
| 28 | + } noraise { |
| 29 | + _ => fail("expected a decode failure", loc~) |
| 30 | + } |
| 31 | + let JsonDecodeError((_, message)) = err |
| 32 | + message |
| 33 | +} |
| 34 | + |
| 35 | +///| |
| 36 | +test "BigInt from_json keeps every digit of a big integer literal" { |
| 37 | + // The literal outruns the exact-integer range of a double, so the parser |
| 38 | + // preserves its text; decoding the rounded double instead would lose the |
| 39 | + // last six digits. |
| 40 | + let json = @json.parse("12345678901234567890123") |
| 41 | + assert_true(json is Number(1.2345678901234568e22, repr=Some(_))) |
| 42 | + inspect(decode(json), content="12345678901234567890123") |
| 43 | + inspect( |
| 44 | + decode(@json.parse("-98765432109876543210987")), |
| 45 | + content="-98765432109876543210987", |
| 46 | + ) |
| 47 | +} |
| 48 | + |
| 49 | +///| |
| 50 | +test "BigInt from_json decodes plain numbers exactly" { |
| 51 | + inspect(decode(Json::number(0)), content="0") |
| 52 | + inspect(decode(Json::number(-0.0)), content="0") |
| 53 | + inspect(decode(Json::number(42)), content="42") |
| 54 | + inspect(decode(Json::number(-42)), content="-42") |
| 55 | + inspect(decode(@json.parse("9007199254740992")), content="9007199254740992") |
| 56 | + inspect(decode(@json.parse("9007199254740993")), content="9007199254740993") |
| 57 | + // 2^63 is one past the top of Int64 (though its negation still fits), so |
| 58 | + // decoding has to go through the significand and exponent rather than a |
| 59 | + // fixed-width integer. |
| 60 | + inspect( |
| 61 | + decode(Json::number(9223372036854775808.0)), |
| 62 | + content="9223372036854775808", |
| 63 | + ) |
| 64 | + inspect( |
| 65 | + decode(Json::number(-9223372036854775808.0)), |
| 66 | + content="-9223372036854775808", |
| 67 | + ) |
| 68 | + inspect( |
| 69 | + decode(Json::number(18446744073709551616.0)), |
| 70 | + content="18446744073709551616", |
| 71 | + ) |
| 72 | + // 2^52 + 1: the first integer needing all 53 significand bits. |
| 73 | + inspect(decode(Json::number(4503599627370497.0)), content="4503599627370497") |
| 74 | + // The largest finite double is (2^53 - 1) * 2^971, the widest shift the |
| 75 | + // conversion ever performs. |
| 76 | + let max_double = decode(Json::number(@double.max_value)) |
| 77 | + assert_eq(max_double, (2N.pow(53N) - 1N) << 971) |
| 78 | + inspect(max_double.to_string().length(), content="309") |
| 79 | +} |
| 80 | + |
| 81 | +///| |
| 82 | +test "BigInt from_json decodes a double no fixed-width integer can hold" { |
| 83 | + // 1e300 is neither an integer literal nor out of the range of a double, so |
| 84 | + // the parser keeps no text for it and the double itself is decoded. That |
| 85 | + // double is not exactly 10^300 but the nearest double to it, and that is the |
| 86 | + // integer that comes out. |
| 87 | + let json = @json.parse("1e300") |
| 88 | + assert_true(json is Number(1.0e300, repr=None)) |
| 89 | + let decoded = decode(json) |
| 90 | + inspect(decoded.to_string().length(), content="301") |
| 91 | + inspect( |
| 92 | + decoded.to_string().view(end_offset=20), |
| 93 | + content="10000000000000000525", |
| 94 | + ) |
| 95 | + assert_eq(decoded % 2N, 0N) |
| 96 | +} |
| 97 | + |
| 98 | +///| |
| 99 | +test "BigInt from_json round trips its own encoding" { |
| 100 | + let values = [0N, 1N, -1N, 12345678901234567890N, -12345678901234567890N] |
| 101 | + for value in values { |
| 102 | + assert_eq(decode(value.to_json()), value) |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +///| |
| 107 | +test "BigInt from_json rejects numbers that are not integers" { |
| 108 | + inspect( |
| 109 | + decode_error(Json::number(1.5)), |
| 110 | + content="BigInt::from_json: number is not an integer", |
| 111 | + ) |
| 112 | + inspect( |
| 113 | + decode_error(Json::number(@double.not_a_number)), |
| 114 | + content="BigInt::from_json: number is not finite", |
| 115 | + ) |
| 116 | + // Every non-zero subnormal is a proper fraction, so none of them decode. |
| 117 | + inspect( |
| 118 | + decode_error(Json::number(@double.min_positive / 2.0)), |
| 119 | + content="BigInt::from_json: number is not an integer", |
| 120 | + ) |
| 121 | + inspect( |
| 122 | + decode_error(Json::number(@double.infinity)), |
| 123 | + content="BigInt::from_json: number is not finite", |
| 124 | + ) |
| 125 | + // An exponent-form literal beyond the range of a double keeps its text, and |
| 126 | + // that text is authoritative: it is not a plain integer literal, so it is |
| 127 | + // rejected rather than decoded from the infinity it rounded to. |
| 128 | + inspect( |
| 129 | + decode_error(@json.parse("1e400")), |
| 130 | + content=( |
| 131 | + #|BigInt::from_json: `1e400` is not a plain integer literal |
| 132 | + ), |
| 133 | + ) |
| 134 | + inspect( |
| 135 | + decode_error(Json::number(1, repr="oops")), |
| 136 | + content=( |
| 137 | + #|BigInt::from_json: `oops` is not a plain integer literal |
| 138 | + ), |
| 139 | + ) |
| 140 | + inspect( |
| 141 | + decode_error(Json("not-a-number")), |
| 142 | + content="BigInt::from_json: invalid number in string representation", |
| 143 | + ) |
| 144 | + inspect( |
| 145 | + decode_error(Json(true)), |
| 146 | + content="BigInt::from_json: expected a number or its string representation", |
| 147 | + ) |
| 148 | +} |
0 commit comments