summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Flynn <trflynn89@pm.me>2022-01-31 10:23:51 -0500
committerLinus Groh <mail@linusgroh.de>2022-01-31 17:50:54 +0000
commit281b0411f2265f97ab97666d446f1a3c65004430 (patch)
tree86755e569af0050494c6e86ad5925b3544c1366f
parent94a346c9b9e34a0c6ae20a2e6877e9a5ce8f1296 (diff)
downloadserenity-281b0411f2265f97ab97666d446f1a3c65004430.zip
LibJS: Implement conversion of strings to BigInts according to the spec
The spec defines a StringToBigInt AO which allows for converting binary, octal, decimal, and hexadecimal strings to a BigInt. Our conversion was only allowing for decimal strings.
-rw-r--r--Userland/Libraries/LibJS/Runtime/Value.cpp84
-rw-r--r--Userland/Libraries/LibJS/Runtime/Value.h1
-rw-r--r--Userland/Libraries/LibJS/Tests/builtins/BigInt/BigInt.js60
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}`);
+ });
+ });
});