diff options
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/Value.cpp | 84 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/Value.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/builtins/BigInt/BigInt.js | 60 |
3 files changed, 141 insertions, 4 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/Value.cpp b/Userland/Libraries/LibJS/Runtime/Value.cpp index 089bda8d69..38fe37b75a 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.cpp +++ b/Userland/Libraries/LibJS/Runtime/Value.cpp @@ -6,6 +6,7 @@ */ #include <AK/AllOf.h> +#include <AK/CharacterTypes.h> #include <AK/String.h> #include <AK/StringBuilder.h> #include <AK/Utf8View.h> @@ -528,10 +529,15 @@ ThrowCompletionOr<BigInt*> Value::to_bigint(GlobalObject& global_object) const case Type::Double: return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "number", "BigInt"); case Type::String: { - auto& string = primitive.as_string().string(); - if (!is_valid_bigint_value(string)) - return vm.throw_completion<SyntaxError>(global_object, ErrorType::BigIntInvalidValue, string); - return js_bigint(vm, Crypto::SignedBigInteger::from_base(10, string.trim_whitespace())); + // 1. Let n be ! StringToBigInt(prim). + auto bigint = primitive.string_to_bigint(global_object); + + // 2. If n is undefined, throw a SyntaxError exception. + if (!bigint.has_value()) + return vm.throw_completion<SyntaxError>(global_object, ErrorType::BigIntInvalidValue, primitive); + + // 3. Return n. + return bigint.release_value(); } case Type::Symbol: return vm.throw_completion<TypeError>(global_object, ErrorType::Convert, "symbol", "BigInt"); @@ -540,6 +546,76 @@ ThrowCompletionOr<BigInt*> Value::to_bigint(GlobalObject& global_object) const } } +struct BigIntParseResult { + StringView literal; + u8 base { 10 }; + bool is_negative { false }; +}; + +static Optional<BigIntParseResult> parse_bigint_text(StringView text) +{ + BigIntParseResult result {}; + + auto parse_for_prefixed_base = [&](auto lower_prefix, auto upper_prefix, auto validator) { + if (text.length() <= 2) + return false; + if (!text.starts_with(lower_prefix) && !text.starts_with(upper_prefix)) + return false; + return all_of(text.substring_view(2), validator); + }; + + if (parse_for_prefixed_base("0b"sv, "0B"sv, is_ascii_binary_digit)) { + result.literal = text.substring_view(2); + result.base = 2; + } else if (parse_for_prefixed_base("0o"sv, "0O"sv, is_ascii_octal_digit)) { + result.literal = text.substring_view(2); + result.base = 8; + } else if (parse_for_prefixed_base("0x"sv, "0X"sv, is_ascii_hex_digit)) { + result.literal = text.substring_view(2); + result.base = 16; + } else { + if (text.starts_with('-')) { + text = text.substring_view(1); + result.is_negative = true; + } else if (text.starts_with('+')) { + text = text.substring_view(1); + } + + if (!all_of(text, is_ascii_digit)) + return {}; + + result.literal = text; + result.base = 10; + } + + return result; +} + +// 7.1.14 StringToBigInt ( str ), https://tc39.es/ecma262/#sec-stringtobigint +Optional<BigInt*> Value::string_to_bigint(GlobalObject& global_object) const +{ + VERIFY(is_string()); + + // 1. Let text be ! StringToCodePoints(str). + auto text = as_string().string().view().trim_whitespace(); + + // 2. Let literal be ParseText(text, StringIntegerLiteral). + auto result = parse_bigint_text(text); + + // 3. If literal is a List of errors, return undefined. + if (!result.has_value()) + return {}; + + // 4. Let mv be the MV of literal. + // 5. Assert: mv is an integer. + auto bigint = Crypto::SignedBigInteger::from_base(result->base, result->literal); + if (result->is_negative && (bigint != BIGINT_ZERO)) + bigint.negate(); + + // 6. Return ℤ(mv). + return js_bigint(global_object.vm(), move(bigint)); +} + // 7.1.15 ToBigInt64 ( argument ), https://tc39.es/ecma262/#sec-tobigint64 ThrowCompletionOr<i64> Value::to_bigint_int64(GlobalObject& global_object) const { diff --git a/Userland/Libraries/LibJS/Runtime/Value.h b/Userland/Libraries/LibJS/Runtime/Value.h index 8bd6ecf412..96ad0b1a94 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.h +++ b/Userland/Libraries/LibJS/Runtime/Value.h @@ -333,6 +333,7 @@ public: ThrowCompletionOr<FunctionObject*> get_method(GlobalObject&, PropertyKey const&) const; String to_string_without_side_effects() const; + Optional<BigInt*> string_to_bigint(GlobalObject& global_object) const; Value value_or(Value fallback) const { diff --git a/Userland/Libraries/LibJS/Tests/builtins/BigInt/BigInt.js b/Userland/Libraries/LibJS/Tests/builtins/BigInt/BigInt.js index 61b5a29a84..33e1ff5ed5 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/BigInt/BigInt.js +++ b/Userland/Libraries/LibJS/Tests/builtins/BigInt/BigInt.js @@ -29,6 +29,40 @@ describe("correct behavior", () => { test("constructor with objects", () => { expect(BigInt([])).toBe(0n); }); + + test("base-2 strings", () => { + expect(BigInt("0b0")).toBe(0n); + expect(BigInt("0B0")).toBe(0n); + expect(BigInt("0b1")).toBe(1n); + expect(BigInt("0B1")).toBe(1n); + expect(BigInt("0b10")).toBe(2n); + expect(BigInt("0B10")).toBe(2n); + expect(BigInt(`0b${"1".repeat(100)}`)).toBe(1267650600228229401496703205375n); + }); + + test("base-8 strings", () => { + expect(BigInt("0o0")).toBe(0n); + expect(BigInt("0O0")).toBe(0n); + expect(BigInt("0o1")).toBe(1n); + expect(BigInt("0O1")).toBe(1n); + expect(BigInt("0o7")).toBe(7n); + expect(BigInt("0O7")).toBe(7n); + expect(BigInt("0o10")).toBe(8n); + expect(BigInt("0O10")).toBe(8n); + expect(BigInt(`0o1${"7".repeat(33)}`)).toBe(1267650600228229401496703205375n); + }); + + test("base-16 strings", () => { + expect(BigInt("0x0")).toBe(0n); + expect(BigInt("0X0")).toBe(0n); + expect(BigInt("0x1")).toBe(1n); + expect(BigInt("0X1")).toBe(1n); + expect(BigInt("0xf")).toBe(15n); + expect(BigInt("0Xf")).toBe(15n); + expect(BigInt("0x10")).toBe(16n); + expect(BigInt("0X10")).toBe(16n); + expect(BigInt(`0x${"f".repeat(25)}`)).toBe(1267650600228229401496703205375n); + }); }); describe("errors", () => { @@ -65,4 +99,30 @@ describe("errors", () => { }).toThrowWithMessage(RangeError, "Cannot convert non-integral number to BigInt"); }); }); + + test("invalid string for base", () => { + ["0b", "0b2", "0B02", "-0b1", "-0B1"].forEach(value => { + expect(() => { + BigInt(value); + }).toThrowWithMessage(SyntaxError, `Invalid value for BigInt: ${value}`); + }); + + ["0o", "0o8", "0O08", "-0o1", "-0O1"].forEach(value => { + expect(() => { + BigInt(value); + }).toThrowWithMessage(SyntaxError, `Invalid value for BigInt: ${value}`); + }); + + ["0x", "0xg", "0X0g", "-0x1", "-0X1"].forEach(value => { + expect(() => { + BigInt(value); + }).toThrowWithMessage(SyntaxError, `Invalid value for BigInt: ${value}`); + }); + + ["a", "-1a"].forEach(value => { + expect(() => { + BigInt(value); + }).toThrowWithMessage(SyntaxError, `Invalid value for BigInt: ${value}`); + }); + }); }); |