From f4b3bb519f4dcf9aa79b68420da4e7250226ecb7 Mon Sep 17 00:00:00 2001 From: Slappy826 Date: Fri, 26 Aug 2022 11:14:04 -0500 Subject: [PATCH] LibJS: Handle non-decimal integer literals in Value::to_number Implements support for parsing binary and octal literals, and fixes instances where a hex literal is parsed in ways the spec doesn't allow. --- Userland/Libraries/LibJS/Runtime/Value.cpp | 107 ++++++++++++++---- .../LibJS/Tests/builtins/Number/Number.js | 84 ++++++++++++++ 2 files changed, 172 insertions(+), 19 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/Value.cpp b/Userland/Libraries/LibJS/Runtime/Value.cpp index a4997dc5ad..eddfe0b4bd 100644 --- a/Userland/Libraries/LibJS/Runtime/Value.cpp +++ b/Userland/Libraries/LibJS/Runtime/Value.cpp @@ -471,6 +471,92 @@ FLATTEN ThrowCompletionOr Value::to_numeric(VM& vm) const return primitive.to_number(vm); } +constexpr bool is_ascii_number(u32 code_point) +{ + return is_ascii_digit(code_point) || code_point == '.' || (code_point == 'e' || code_point == 'E') || code_point == '+' || code_point == '-'; +} + +struct NumberParseResult { + StringView literal; + u8 base; +}; + +static Optional parse_number_text(StringView text) +{ + NumberParseResult result {}; + + auto check_prefix = [&](auto lower_prefix, auto upper_prefix) { + if (text.length() <= 2) + return false; + if (!text.starts_with(lower_prefix) && !text.starts_with(upper_prefix)) + return false; + return true; + }; + + // https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type + if (check_prefix("0b"sv, "0B"sv)) { + if (!all_of(text.substring_view(2), is_ascii_binary_digit)) + return {}; + + result.literal = text.substring_view(2); + result.base = 2; + } else if (check_prefix("0o"sv, "0O"sv)) { + if (!all_of(text.substring_view(2), is_ascii_octal_digit)) + return {}; + + result.literal = text.substring_view(2); + result.base = 8; + } else if (check_prefix("0x"sv, "0X"sv)) { + if (!all_of(text.substring_view(2), is_ascii_hex_digit)) + return {}; + + result.literal = text.substring_view(2); + result.base = 16; + } else { + if (!all_of(text, is_ascii_number)) + return {}; + + result.literal = text; + result.base = 10; + } + + return result; +} + +// 7.1.4.1.1 StringToNumber ( str ), https://tc39.es/ecma262/#sec-stringtonumber +static Optional string_to_number(StringView string) +{ + // 1. Let text be StringToCodePoints(str). + String text = string.trim_whitespace(); + + // 2. Let literal be ParseText(text, StringNumericLiteral). + if (text.is_empty()) + return Value(0); + if (text == "Infinity" || text == "+Infinity") + return js_infinity(); + if (text == "-Infinity") + return js_negative_infinity(); + + auto result = parse_number_text(text); + + // 3. If literal is a List of errors, return NaN. + if (!result.has_value()) + return js_nan(); + + // 4. Return StringNumericValue of literal. + if (result->base != 10) { + auto bigint = Crypto::UnsignedBigInteger::from_base(result->base, result->literal); + return Value(bigint.to_double()); + } + + char* endptr; + auto parsed_double = strtod(text.characters(), &endptr); + if (*endptr) + return js_nan(); + + return Value(parsed_double); +} + // 7.1.4 ToNumber ( argument ), https://tc39.es/ecma262/#sec-tonumber ThrowCompletionOr Value::to_number(VM& vm) const { @@ -485,25 +571,8 @@ ThrowCompletionOr Value::to_number(VM& vm) const return Value(0); case BOOLEAN_TAG: return Value(as_bool() ? 1 : 0); - case STRING_TAG: { - String string = Utf8View(as_string().string()).trim(whitespace_characters, AK::TrimMode::Both).as_string(); - if (string.is_empty()) - return Value(0); - if (string == "Infinity" || string == "+Infinity") - return js_infinity(); - if (string == "-Infinity") - return js_negative_infinity(); - char* endptr; - auto parsed_double = strtod(string.characters(), &endptr); - if (*endptr) - return js_nan(); - // NOTE: Per the spec only exactly [+-]Infinity should result in infinity - // but strtod gives infinity for any case-insensitive 'infinity' or 'inf' string. - if (isinf(parsed_double) && string.contains('i', AK::CaseSensitivity::CaseInsensitive)) - return js_nan(); - - return Value(parsed_double); - } + case STRING_TAG: + return string_to_number(as_string().string().view()); case SYMBOL_TAG: return vm.throw_completion(ErrorType::Convert, "symbol", "number"); case BIGINT_TAG: diff --git a/Userland/Libraries/LibJS/Tests/builtins/Number/Number.js b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.js index 7a237dd21b..e8e9f19096 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Number/Number.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.js @@ -11,6 +11,9 @@ test("constructor without new", () => { expect(Number(-123)).toBe(-123); expect(Number(123n)).toBe(123); expect(Number(-123n)).toBe(-123); + expect(Number("1_23")).toBeNaN(); + expect(Number("00123")).toBe(123); + expect(Number("123n")).toBeNaN(); expect(Number("42")).toBe(42); expect(Number(null)).toBe(0); expect(Number(true)).toBe(1); @@ -29,6 +32,45 @@ test("constructor without new", () => { expect(Number("foo")).toBeNaN(); expect(Number("10e10000")).toBe(Infinity); expect(Number("-10e10000")).toBe(-Infinity); + expect(Number("0b1")).toBe(1); + expect(Number("0B1")).toBe(1); + expect(Number("0b01")).toBe(1); + expect(Number("0b11")).toBe(3); + expect(Number("0b")).toBeNaN(); + expect(Number("0B")).toBeNaN(); + expect(Number("-0b1")).toBeNaN(); + expect(Number("+0b1")).toBeNaN(); + expect(Number("0b1.1")).toBeNaN(); + expect(Number("0b1e10")).toBeNaN(); + expect(Number("0b1e+10")).toBeNaN(); + expect(Number("0b1e-10")).toBeNaN(); + expect(Number("0b1_1")).toBeNaN(); + expect(Number("0o7")).toBe(7); + expect(Number("0O7")).toBe(7); + expect(Number("0o07")).toBe(7); + expect(Number("0o77")).toBe(63); + expect(Number("0o")).toBeNaN(); + expect(Number("0O")).toBeNaN(); + expect(Number("-0o1")).toBeNaN(); + expect(Number("+0o1")).toBeNaN(); + expect(Number("0o1.1")).toBeNaN(); + expect(Number("0o1e10")).toBeNaN(); + expect(Number("0o1e+10")).toBeNaN(); + expect(Number("0o1e-10")).toBeNaN(); + expect(Number("0o1_1")).toBeNaN(); + expect(Number("0x1")).toBe(1); + expect(Number("0X1")).toBe(1); + expect(Number("0x01")).toBe(1); + expect(Number("0x11")).toBe(17); + expect(Number("0x")).toBeNaN(); + expect(Number("0X")).toBeNaN(); + expect(Number("-0x1")).toBeNaN(); + expect(Number("+0x1")).toBeNaN(); + expect(Number("0x1.1")).toBeNaN(); + expect(Number("0x1e10")).toBe(7696); + expect(Number("0x1e+10")).toBeNaN(); + expect(Number("0x1e-10")).toBeNaN(); + expect(Number("0x1_1")).toBeNaN(); }); test("constructor with new", () => { @@ -39,6 +81,9 @@ test("constructor with new", () => { expect(new Number(-123).valueOf()).toBe(-123); expect(new Number(123n).valueOf()).toBe(123); expect(new Number(-123n).valueOf()).toBe(-123); + expect(new Number("1_23").valueOf()).toBeNaN(); + expect(new Number("00123").valueOf()).toBe(123); + expect(new Number("123n").valueOf()).toBeNaN(); expect(new Number("42").valueOf()).toBe(42); expect(new Number(null).valueOf()).toBe(0); expect(new Number(true).valueOf()).toBe(1); @@ -57,4 +102,43 @@ test("constructor with new", () => { expect(new Number("foo").valueOf()).toBeNaN(); expect(new Number("10e10000").valueOf()).toBe(Infinity); expect(new Number("-10e10000").valueOf()).toBe(-Infinity); + expect(new Number("0b1").valueOf()).toBe(1); + expect(new Number("0B1").valueOf()).toBe(1); + expect(new Number("0b01").valueOf()).toBe(1); + expect(new Number("0b11").valueOf()).toBe(3); + expect(new Number("0b").valueOf()).toBeNaN(); + expect(new Number("0B").valueOf()).toBeNaN(); + expect(new Number("-0b1").valueOf()).toBeNaN(); + expect(new Number("+0b1").valueOf()).toBeNaN(); + expect(new Number("0b1.1").valueOf()).toBeNaN(); + expect(new Number("0b1e10").valueOf()).toBeNaN(); + expect(new Number("0b1e+10").valueOf()).toBeNaN(); + expect(new Number("0b1e-10").valueOf()).toBeNaN(); + expect(new Number("0b1_1").valueOf()).toBeNaN(); + expect(new Number("0o7").valueOf()).toBe(7); + expect(new Number("0O7").valueOf()).toBe(7); + expect(new Number("0o07").valueOf()).toBe(7); + expect(new Number("0o77").valueOf()).toBe(63); + expect(new Number("0o").valueOf()).toBeNaN(); + expect(new Number("0O").valueOf()).toBeNaN(); + expect(new Number("-0o1").valueOf()).toBeNaN(); + expect(new Number("+0o1").valueOf()).toBeNaN(); + expect(new Number("0o1.1").valueOf()).toBeNaN(); + expect(new Number("0o1e7").valueOf()).toBeNaN(); + expect(new Number("0o1e+10").valueOf()).toBeNaN(); + expect(new Number("0o1e-10").valueOf()).toBeNaN(); + expect(new Number("0o1_1").valueOf()).toBeNaN(); + expect(new Number("0x1").valueOf()).toBe(1); + expect(new Number("0X1").valueOf()).toBe(1); + expect(new Number("0x01").valueOf()).toBe(1); + expect(new Number("0x11").valueOf()).toBe(17); + expect(new Number("0x").valueOf()).toBeNaN(); + expect(new Number("0X").valueOf()).toBeNaN(); + expect(new Number("-0x1").valueOf()).toBeNaN(); + expect(new Number("+0x1").valueOf()).toBeNaN(); + expect(new Number("0x1.1").valueOf()).toBeNaN(); + expect(new Number("0x1e10").valueOf()).toBe(7696); + expect(new Number("0x1e+10").valueOf()).toBeNaN(); + expect(new Number("0x1e-10").valueOf()).toBeNaN(); + expect(new Number("0x1_1").valueOf()).toBeNaN(); });