diff --git a/examples/results.txt b/examples/results.txt index 73d4f9a..79ff3aa 100644 --- a/examples/results.txt +++ b/examples/results.txt @@ -931,7 +931,7 @@ compat-table/es6/let.strict.no-statement.js: failed compat-table/es6/let.strict.shadow.js: OK compat-table/es6/let.strict.tdz.js: OK compat-table/es6/let.tdz.js: OK -compat-table/es6/literals.binary.Number.js: failed +compat-table/es6/literals.binary.Number.js: OK compat-table/es6/literals.binary.js: OK compat-table/es6/literals.object.computed-accessors.js: OK compat-table/es6/literals.object.computed-properties.js: OK @@ -939,7 +939,7 @@ compat-table/es6/literals.object.computed-shorthand-methods.js: OK compat-table/es6/literals.object.shorthand-methods.js: OK compat-table/es6/literals.object.shorthand-properties.js: OK compat-table/es6/literals.object.string-keyed-shorthand-methods.js: OK -compat-table/es6/literals.octal.Number.js: failed +compat-table/es6/literals.octal.Number.js: OK compat-table/es6/literals.octal.js: OK compat-table/es6/misc.Invalid-Date.js: OK compat-table/es6/misc.Object.freeze.primitives.js: OK @@ -1115,7 +1115,7 @@ compat-table/es6/template.permanent-caching.js: OK compat-table/es6/template.tagged.js: OK compat-table/es6/template.toString.js: OK compat-table/es6/typed-arrays.ArrayBuffer.Symbol.species.js: OK -compat-table/es6/typed-arrays.DataView.Float32.js: failed +compat-table/es6/typed-arrays.DataView.Float32.js: OK compat-table/es6/typed-arrays.DataView.Float64.js: OK compat-table/es6/typed-arrays.DataView.Int8.js: OK compat-table/es6/typed-arrays.DataView.Int16.js: OK @@ -1123,7 +1123,7 @@ compat-table/es6/typed-arrays.DataView.Int32.js: OK compat-table/es6/typed-arrays.DataView.Uint8.js: OK compat-table/es6/typed-arrays.DataView.Uint16.js: OK compat-table/es6/typed-arrays.DataView.Uint32.js: OK -compat-table/es6/typed-arrays.Float32Array.js: failed +compat-table/es6/typed-arrays.Float32Array.js: OK compat-table/es6/typed-arrays.Float64Array.js: OK compat-table/es6/typed-arrays.Int8Array.js: OK compat-table/es6/typed-arrays.Int16Array.js: OK diff --git a/examples/spec/numbers.js b/examples/spec/numbers.js index d1f92b9..f98de4a 100644 --- a/examples/spec/numbers.js +++ b/examples/spec/numbers.js @@ -28,6 +28,8 @@ test('numeric separator', 1_000_000, 1000000); test('Number()', Number('42'), 42); test('Number() float', Number('3.14'), 3.14); +test('Number() empty string', Number(''), 0); +test('Number() signed hex invalid', Number.isNaN(Number('+0x10')), true); test('Number() invalid', Number.isNaN(Number('abc')), true); test('parseInt', parseInt('42'), 42); @@ -60,6 +62,11 @@ test('Number.isSafeInteger true', Number.isSafeInteger(42), true); test('Number.isSafeInteger false', Number.isSafeInteger(9007199254740992), false); test('toFixed', (3.14159).toFixed(2), '3.14'); +test('decimal literal matches Number string parse', 0.7875 === Number('0.7875'), true); +test('toFixed halfway below', (0.7875).toFixed(3), '0.787'); +test('toFixed halfway above', (0.7876).toFixed(3), '0.788'); +test('toFixed exposes correctly parsed double', (0.7875).toFixed(20), '0.78749999999999997780'); +test('toFixed binary midpoint', (2.675).toFixed(2), '2.67'); test('toPrecision', (123.456).toPrecision(4), '123.5'); test('toExponential', (12345).toExponential(2), '1.23e+4'); test('toString', (255).toString(16), 'ff'); @@ -67,6 +74,9 @@ test('integer literal method access with double dot', 27..toString(), '27'); test('integer literal method access with spaced dot', 27 .toString(), '27'); test('exponent literal method access', 1e3.toString(), '1000'); test('leading-dot literal method access', .5.toString(), '0.5'); +testEvalSyntaxError('missing exponent digits syntax error', '1e'); +testEvalSyntaxError('missing signed exponent digits syntax error', '1e+'); +testEvalSyntaxError('dot exponent without digits syntax error', '1.e'); testEvalSyntaxError('integer literal dot identifier syntax error', '27.toString()'); testEvalSyntaxError('integer literal dot property syntax error', '27.a'); testEvalSyntaxError('leading-dot literal identifier syntax error', '.5foo'); diff --git a/include/numbers.h b/include/numbers.h new file mode 100644 index 0000000..a31223a --- /dev/null +++ b/include/numbers.h @@ -0,0 +1,31 @@ +#ifndef ANT_NUMBER_CONVERSION_H +#define ANT_NUMBER_CONVERSION_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + ANT_NUMBER_PARSE_DECIMAL, + ANT_NUMBER_PARSE_JS_NUMBER, + ANT_NUMBER_PARSE_FLOAT_PREFIX, +} ant_number_parse_mode_t; + +bool ant_number_parse( + const char *str, size_t len, + ant_number_parse_mode_t mode, + double *out, size_t *processed +); + +size_t ant_number_to_shortest(double value, char *buf, size_t len); +size_t ant_number_to_fixed(double value, int digits, char *buf, size_t len); +size_t ant_number_to_precision(double value, int precision, char *buf, size_t len); +size_t ant_number_to_exponential(double value, int digits, char *buf, size_t len); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/meson.build b/meson.build index 7fe3bb2..477118d 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,8 @@ -project('ant', 'c', default_options: [ +project('ant', ['c', 'cpp'], default_options: [ 'buildtype=release', 'optimization=3', 'c_std=gnu23', + 'cpp_std=gnu++23', 'default_library=static', 'b_lto=true', 'b_lto_threads=8', diff --git a/meson/deps/meson.build b/meson/deps/meson.build index 0bb42db..6b2f28f 100644 --- a/meson/deps/meson.build +++ b/meson/deps/meson.build @@ -126,6 +126,7 @@ minicoro_dep = subproject('minicoro').get_variable('minicoro_dep') crprintf_dep = subproject('crprintf').get_variable('crprintf_dep') uriparser_dep = subproject('uriparser').get_variable('uriparser_dep') wamr_dep = subproject('wasm-micro-runtime').get_variable('wamr_dep') +double_conversion_dep = subproject('double-conversion').get_variable('double_conversion_dep') utf8proc_base_dep = subproject('utf8proc').get_variable('utf8proc_dep') utf8proc_dep = declare_dependency( @@ -234,6 +235,7 @@ ant_deps = [ uriparser_dep, utf8proc_dep, ssl_dep, crypto_dep, zlib_dep, brotli_common_dep, brotli_dec_dep, brotli_enc_dep, uthash_dep, lmdb_dep, wamr_dep, + double_conversion_dep, ] + win_deps if get_option('jit') diff --git a/sources.json b/sources.json index 97345b7..67e1b2b 100644 --- a/sources.json +++ b/sources.json @@ -2,6 +2,7 @@ "engine": { "patterns": [ "src/*.c", + "src/*.cc", "src/gc/*.c", "src/highlight/*.c", "src/esm/*.c", diff --git a/src/ant.c b/src/ant.c index e218d63..8f4fcc9 100644 --- a/src/ant.c +++ b/src/ant.c @@ -18,6 +18,7 @@ #include "errors.h" #include "descriptors.h" #include "shapes.h" +#include "numbers.h" #include "gc.h" #include "gc/objects.h" @@ -1932,76 +1933,16 @@ static size_t strobj(ant_t *js, ant_value_t obj, char *buf, size_t len) { return n; } -static size_t fix_exponent(char *buf, size_t n) { - char *e = strchr(buf, 'e'); - if (!e) return n; - - char *src = e + 1; - char *dst = src; - - if (*src == '+' || *src == '-') { - dst++; - src++; - } - - while (*src == '0' && src[1] != '\0') src++; - - if (src != dst) { - memmove(dst, src, strlen(src) + 1); - return strlen(buf); - } - return n; -} - -static size_t strnum_safe_integer(double dv, char *buf, size_t len) { - char temp[32]; - size_t pos = sizeof(temp); - uint64_t value = 0; - bool negative = signbit(dv) != 0; - - if (negative) dv = -dv; - value = (uint64_t)dv; - - do { - temp[--pos] = (char)('0' + (value % 10u)); - value /= 10u; - } while (value != 0); - - if (negative) temp[--pos] = '-'; - return cpy(buf, len, temp + pos, sizeof(temp) - pos); -} - static size_t strnum(ant_value_t value, char *buf, size_t len) { double dv = tod(value); - if (dv == 0.0) return cpy(buf, len, "0", 1); if (__builtin_expect(isnan(dv), 0)) return cpy(buf, len, "NaN", 3); + if (__builtin_expect(isinf(dv), 0)) return cpy(buf, len, dv > 0 ? "Infinity" : "-Infinity", dv > 0 ? 8 : 9); - - char temp[64]; - int sign = dv < 0 ? 1 : 0; - double adv = sign ? -dv : dv; - - double iv; - double frac = modf(adv, &iv); - if (frac == 0.0 && adv < 9007199254740992.0) { - return strnum_safe_integer(dv, buf, len); - } - - for (int prec = 1; prec <= 17; prec++) { - int n = snprintf(temp, sizeof(temp), "%.*g", prec, dv); - double parsed = strtod(temp, NULL); - if (parsed == dv) { - fix_exponent(temp, (size_t)n); - return cpy(buf, len, temp, strlen(temp)); - } - } - - int result = snprintf(temp, sizeof(temp), "%.17g", dv); - fix_exponent(temp, (size_t)result); - return cpy(buf, len, temp, strlen(temp)); + + return ant_number_to_shortest(dv, buf, len); } static inline ant_offset_t assert_flat_string_len(ant_value_t value, const char **out_ptr) { @@ -3315,50 +3256,6 @@ static const char *get_func_code(ant_t *js, ant_value_t func_obj, ant_offset_t * return (const char *)(uintptr_t)vdata(code_val); } -static inline bool js_is_trim_space(char ch) { - return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; -} - -static inline bool js_try_parse_ascii_decimal_fast(const char *str, size_t len, double *out) { - size_t i = 0; - int sign = 1; - int int_digits = 0; - int frac_digits = 0; - - double value = 0.0; - double scale = 1.0; - bool saw_digit = false; - - if (len == 0) return false; - if (str[i] == '+' || str[i] == '-') { - sign = (str[i] == '-') ? -1 : 1; - i++; - if (i == len) return false; - } - - while (i < len && str[i] >= '0' && str[i] <= '9') { - if (int_digits >= 18) return false; - value = value * 10.0 + (double)(str[i] - '0'); - saw_digit = true; - int_digits++; i++; - } - - if (i < len && str[i] == '.') { - i++; - while (i < len && str[i] >= '0' && str[i] <= '9') { - if (frac_digits >= 18) return false; - scale *= 0.1; - value += (double)(str[i] - '0') * scale; - saw_digit = true; - frac_digits++; i++; - }} - - if (!saw_digit || i != len) return false; - *out = (sign < 0) ? -value : value; - - return true; -} - double js_to_number(ant_t *js, ant_value_t arg) { if (vtype(arg) == T_NULL) return 0.0; if (vtype(arg) == T_UNDEF) return JS_NAN; @@ -3370,33 +3267,24 @@ double js_to_number(ant_t *js, ant_value_t arg) { if (vtype(arg) == T_STR) { ant_flat_string_t *flat = ant_str_flat_ptr(arg); const char *base = NULL; - const char *s = NULL; const char *end = NULL; if (flat) { base = flat->bytes; - s = base; end = base + flat->len; } else { ant_offset_t len = 0; ant_offset_t off = vstr(js, arg, &len); base = (const char *)(uintptr_t)off; - s = base; end = base + len; } - while (s < end && js_is_trim_space(*s)) s++; - if (s == end) return 0.0; + double val = JS_NAN; if ( + ant_number_parse(base, (size_t)(end - base), + ANT_NUMBER_PARSE_JS_NUMBER, &val, NULL) + ) return val; - double fast_val = 0.0; - if (js_try_parse_ascii_decimal_fast(s, (size_t)(end - s), &fast_val)) - return fast_val; - - char *parse_end = NULL; - double val = strtod(s, &parse_end); - while (parse_end < end && js_is_trim_space(*parse_end)) parse_end++; - - return (parse_end == s || parse_end != end) ? JS_NAN : val; + return JS_NAN; } if (vtype(arg) == T_OBJ || vtype(arg) == T_ARR) { @@ -11650,49 +11538,17 @@ static ant_value_t builtin_number_toFixed(ant_t *js, ant_value_t *args, int narg } } - bool negative = d < 0; - if (negative) d = -d; - - if (d >= 1e21) { + if (fabs(d) >= 1e21) { char buf[64]; - snprintf(buf, sizeof(buf), "%.0f", negative ? -d : d); - return js_mkstr(js, buf, strlen(buf)); - } - - double scale = pow(10, digits); - double scaled = d * scale; - double rounded = floor(scaled + 0.5); - - char digit_buf[128]; - snprintf(digit_buf, sizeof(digit_buf), "%.0f", rounded); - int digit_len = (int)strlen(digit_buf); - - while (digit_len < digits + 1) { - memmove(digit_buf + 1, digit_buf, digit_len + 1); - digit_buf[0] = '0'; - digit_len++; - } - - char buf[128]; - int pos = 0; - - if (negative && rounded != 0) buf[pos++] = '-'; - int int_digits = digit_len - digits; - if (int_digits <= 0) int_digits = 1; - - for (int i = 0; i < int_digits; i++) { - buf[pos++] = digit_buf[i]; - } - - if (digits > 0) { - buf[pos++] = '.'; - for (int i = int_digits; i < digit_len; i++) { - buf[pos++] = digit_buf[i]; - } + size_t len = strnum(num, buf, sizeof(buf)); + return js_mkstr(js, buf, len); } + + char buf[160]; + size_t len = ant_number_to_fixed(d, digits, buf, sizeof(buf)); + if (len == 0) return js_mkerr(js, "number formatting failed"); - buf[pos] = '\0'; - return js_mkstr(js, buf, pos); + return js_mkstr(js, buf, len); } static ant_value_t builtin_number_toPrecision(ant_t *js, ant_value_t *args, int nargs) { @@ -11714,90 +11570,11 @@ static ant_value_t builtin_number_toPrecision(ant_t *js, ant_value_t *args, int return js_mkerr_typed(js, JS_ERR_RANGE, "toPrecision() argument must be between 1 and 100"); } - bool negative = d < 0; - if (negative) d = -d; - - if (d == 0) { - char buf[128]; - int pos = 0; - if (negative) buf[pos++] = '-'; - buf[pos++] = '0'; - if (precision > 1) { - buf[pos++] = '.'; - for (int i = 1; i < precision; i++) buf[pos++] = '0'; - } - buf[pos] = '\0'; - return js_mkstr(js, buf, pos); - } + char buf[160]; + size_t len = ant_number_to_precision(d, precision, buf, sizeof(buf)); + if (len == 0) return js_mkerr(js, "number formatting failed"); - int exp = (int) floor(log10(d)); - bool use_exp = (exp < -(precision - 1) - 1) || (exp >= precision); - - if (use_exp) { - double mantissa = d / pow(10, exp); - double scale = pow(10, precision - 1); - double rounded = floor(mantissa * scale + 0.5); - - if (rounded >= scale * 10) { - rounded /= 10; - exp++; - } - - char digit_buf[32]; - snprintf(digit_buf, sizeof(digit_buf), "%.0f", rounded); - int digit_len = (int)strlen(digit_buf); - - char buf[128]; - int pos = 0; - if (negative) buf[pos++] = '-'; - buf[pos++] = digit_buf[0]; - if (precision > 1) { - buf[pos++] = '.'; - for (int i = 1; i < precision; i++) { - buf[pos++] = (i < digit_len) ? digit_buf[i] : '0'; - } - } - buf[pos++] = 'e'; - buf[pos++] = (exp >= 0) ? '+' : '-'; - if (exp < 0) exp = -exp; - snprintf(buf + pos, sizeof(buf) - pos, "%d", exp); - return js_mkstr(js, buf, strlen(buf)); - } else { - int digits_after_point = precision - exp - 1; - if (digits_after_point < 0) digits_after_point = 0; - - double scale = pow(10, digits_after_point); - double rounded = floor(d * scale + 0.5); - - char digit_buf[64]; - snprintf(digit_buf, sizeof(digit_buf), "%.0f", rounded); - int digit_len = (int)strlen(digit_buf); - - while (digit_len < digits_after_point + 1) { - memmove(digit_buf + 1, digit_buf, digit_len + 1); - digit_buf[0] = '0'; - digit_len++; - } - - char buf[128]; - int pos = 0; - if (negative) buf[pos++] = '-'; - - int int_digits = digit_len - digits_after_point; - for (int i = 0; i < int_digits; i++) { - buf[pos++] = digit_buf[i]; - } - - if (digits_after_point > 0) { - buf[pos++] = '.'; - for (int i = int_digits; i < digit_len; i++) { - buf[pos++] = digit_buf[i]; - } - } - - buf[pos] = '\0'; - return js_mkstr(js, buf, pos); - } + return js_mkstr(js, buf, len); } static ant_value_t builtin_number_toExponential(ant_t *js, ant_value_t *args, int nargs) { @@ -11816,69 +11593,11 @@ static ant_value_t builtin_number_toExponential(ant_t *js, ant_value_t *args, in } } - bool negative = d < 0; - if (negative) d = -d; - - int exp = 0; - if (d != 0) { - exp = (int) floor(log10(d)); - double test = d / pow(10, exp); - if (test >= 10) { exp++; test /= 10; } - if (test < 1) { exp--; test *= 10; } - } - - if (digits < 0) { - char temp[32]; - snprintf(temp, sizeof(temp), "%.15g", d); - int sig = 0; - for (int i = 0; temp[i] && temp[i] != 'e' && temp[i] != 'E'; i++) { - if (temp[i] == '.') continue; - if (temp[i] >= '0' && temp[i] <= '9') if (temp[i] != '0' || sig > 0) sig++; - } - digits = sig > 0 ? sig - 1 : 0; - if (digits > 20) digits = 20; - } - - double mantissa = d / pow(10, exp); - double scale = pow(10, digits); - double scaled = mantissa * scale; - double rounded = floor(scaled + 0.5); - - if (rounded >= scale * 10) { - rounded /= 10; - exp++; - } - - char buf[64]; - int pos = 0; - - if (negative) buf[pos++] = '-'; - - char digit_buf[32]; - snprintf(digit_buf, sizeof(digit_buf), "%.0f", rounded); - int digit_len = (int)strlen(digit_buf); - - while (digit_len < digits + 1) { - memmove(digit_buf + 1, digit_buf, digit_len + 1); - digit_buf[0] = '0'; - digit_len++; - } - - buf[pos++] = digit_buf[0]; - - if (digits > 0) { - buf[pos++] = '.'; - for (int i = 1; i <= digits; i++) { - buf[pos++] = (i < digit_len) ? digit_buf[i] : '0'; - } - } - - buf[pos++] = 'e'; - buf[pos++] = (exp >= 0) ? '+' : '-'; - if (exp < 0) exp = -exp; - snprintf(buf + pos, sizeof(buf) - pos, "%d", exp); + char buf[160]; + size_t len = ant_number_to_exponential(d, digits, buf, sizeof(buf)); + if (len == 0) return js_mkerr(js, "number formatting failed"); - return js_mkstr(js, buf, strlen(buf)); + return js_mkstr(js, buf, len); } static ant_value_t builtin_number_valueOf(ant_t *js, ant_value_t *args, int nargs) { @@ -12022,11 +11741,16 @@ static ant_value_t builtin_parseFloat(ant_t *js, ant_value_t *args, int nargs) { if (i >= str_len) return tov(JS_NAN); - char *end; - double result = strtod(&str[i], &end); - - if (end == &str[i]) return tov(JS_NAN); + double result = JS_NAN; + size_t processed = 0; + if (!ant_number_parse( + str + i, + (size_t)(str_len - i), + ANT_NUMBER_PARSE_FLOAT_PREFIX, + &result, &processed + ) || processed == 0) return tov(JS_NAN); + return tov(result); } diff --git a/src/numbers.cc b/src/numbers.cc new file mode 100644 index 0000000..7ddc5ec --- /dev/null +++ b/src/numbers.cc @@ -0,0 +1,249 @@ +#include "numbers.h" + +#include +#include + +#include "double-conversion/utils.h" +#include "double-conversion/double-to-string.h" +#include "double-conversion/string-to-double.h" + +using double_conversion::StringBuilder; +using double_conversion::DoubleToStringConverter; +using double_conversion::StringToDoubleConverter; + +struct JsTrimToken { + const char *bytes; + size_t len; +}; + +static constexpr size_t kShortestBufferSize = + DoubleToStringConverter::kMaxCharsEcmaScriptShortest + 1; + +static constexpr size_t kFixedBufferSize = + 1 + DoubleToStringConverter::kMaxFixedDigitsBeforePoint + 1 + + DoubleToStringConverter::kMaxFixedDigitsAfterPoint + 1; + +static constexpr size_t kPrecisionBufferSize = + DoubleToStringConverter::kMaxPrecisionDigits + 7 + 1; + +static constexpr size_t kExponentialBufferSize = + DoubleToStringConverter::kMaxExponentialDigits + 8 + 1; + +#define JS_TRIM_TOKEN(bytes) { bytes, sizeof(bytes) - 1 } +static constexpr JsTrimToken kJsStringTrimTokens[] = { + JS_TRIM_TOKEN("\t"), + JS_TRIM_TOKEN("\n"), + JS_TRIM_TOKEN("\v"), + JS_TRIM_TOKEN("\f"), + JS_TRIM_TOKEN("\r"), + JS_TRIM_TOKEN(" "), + JS_TRIM_TOKEN("\xc2""\xa0"), + JS_TRIM_TOKEN("\xe1""\x9a""\x80"), + JS_TRIM_TOKEN("\xe2""\x80""\x80"), + JS_TRIM_TOKEN("\xe2""\x80""\x81"), + JS_TRIM_TOKEN("\xe2""\x80""\x82"), + JS_TRIM_TOKEN("\xe2""\x80""\x83"), + JS_TRIM_TOKEN("\xe2""\x80""\x84"), + JS_TRIM_TOKEN("\xe2""\x80""\x85"), + JS_TRIM_TOKEN("\xe2""\x80""\x86"), + JS_TRIM_TOKEN("\xe2""\x80""\x87"), + JS_TRIM_TOKEN("\xe2""\x80""\x88"), + JS_TRIM_TOKEN("\xe2""\x80""\x89"), + JS_TRIM_TOKEN("\xe2""\x80""\x8a"), + JS_TRIM_TOKEN("\xe2""\x80""\xa8"), + JS_TRIM_TOKEN("\xe2""\x80""\xa9"), + JS_TRIM_TOKEN("\xe2""\x80""\xaf"), + JS_TRIM_TOKEN("\xe2""\x81""\x9f"), + JS_TRIM_TOKEN("\xe3""\x80""\x80"), + JS_TRIM_TOKEN("\xef""\xbb""\xbf"), +}; +#undef JS_TRIM_TOKEN + +static size_t js_string_trim_prefix_len(const char *str, size_t len) { + for (const JsTrimToken &token : kJsStringTrimTokens) + if (len >= token.len && std::memcmp(str, token.bytes, token.len) == 0) return token.len; + return 0; +} + +static size_t js_string_trim_suffix_len(const char *str, size_t len) { + for (const JsTrimToken &token : kJsStringTrimTokens) + if (len >= token.len && std::memcmp(str + len - token.len, token.bytes, token.len) == 0) return token.len; + return 0; +} + +static void trim_js_string_whitespace(const char **str, size_t *len, bool trim_trailing, size_t *leading) { + size_t lead = 0; + + while (*len > 0) { + size_t n = js_string_trim_prefix_len(*str, *len); + if (n == 0) break; + *str += n; *len -= n; lead += n; + } + + while (trim_trailing && *len > 0) { + size_t n = js_string_trim_suffix_len(*str, *len); + if (n == 0) break; + *len -= n; + } + + if (leading) *leading = lead; +} + +static bool ant_starts_with_nondecimal_prefix(const char *str, size_t len) { + return + len >= 2 && str[0] == '0' && + ((str[1] | 0x20) == 'x' || + (str[1] | 0x20) == 'b' || (str[1] | 0x20) == 'o'); +} + +static bool ant_parse_radix_integer(const char *str, size_t len, int radix, double *out) { + if (!str || len == 0 || !out) return false; + double value = 0.0; + + for (size_t i = 0; i < len; i++) { + unsigned char ch = (unsigned char)str[i]; + int digit = -1; + if (ch >= '0' && ch <= '9') digit = ch - '0'; + else if (ch >= 'a' && ch <= 'z') digit = ch - 'a' + 10; + else if (ch >= 'A' && ch <= 'Z') digit = ch - 'A' + 10; + if (digit < 0 || digit >= radix) return false; + value = value * (double)radix + (double)digit; + } + + *out = value; + return true; +} + +static bool ant_parse_js_number_prefix(const char *str, size_t len, double *out) { + if (len < 3 || str[0] != '0') return false; + + char kind = (char)(str[1] | 0x20); + int radix = kind == 'b' ? 2 : (kind == 'o' ? 8 : 0); + if (radix == 0) return false; + + double value = 0.0; + if (!ant_parse_radix_integer(str + 2, len - 2, radix, &value)) + return false; + + *out = value; + return true; +} + +extern "C" bool ant_number_parse( + const char *str, size_t len, + ant_number_parse_mode_t mode, + double *out, size_t *processed +) { + if (processed) *processed = 0; + if (!str || !out) return false; + + size_t leading = 0; + if (mode == ANT_NUMBER_PARSE_JS_NUMBER || mode == ANT_NUMBER_PARSE_FLOAT_PREFIX) { + trim_js_string_whitespace(&str, &len, mode == ANT_NUMBER_PARSE_JS_NUMBER, &leading); + + if (mode == ANT_NUMBER_PARSE_JS_NUMBER && len == 0) { + *out = 0.0; + if (processed) *processed = leading; + return true; + }} + + if ( + mode == ANT_NUMBER_PARSE_JS_NUMBER && len >= 3 && + (str[0] == '+' || str[0] == '-') && + ant_starts_with_nondecimal_prefix(str + 1, len - 1) + ) return false; + + if (mode == ANT_NUMBER_PARSE_JS_NUMBER && ant_parse_js_number_prefix(str, len, out)) { + if (processed) *processed = len; + return true; + } + + int flags = 0; + if (mode == ANT_NUMBER_PARSE_JS_NUMBER) + flags = + StringToDoubleConverter::ALLOW_HEX | + StringToDoubleConverter::ALLOW_LEADING_SPACES | + StringToDoubleConverter::ALLOW_TRAILING_SPACES; + else if (mode == ANT_NUMBER_PARSE_FLOAT_PREFIX) + flags = + StringToDoubleConverter::ALLOW_LEADING_SPACES | + StringToDoubleConverter::ALLOW_TRAILING_JUNK; + + StringToDoubleConverter converter( + flags, 0.0, + std::numeric_limits::quiet_NaN(), + "Infinity", "NaN" + ); + + int count = 0; + double value = converter.StringToDouble(str, (int)len, &count); + + if (count <= 0) return false; + if (mode != ANT_NUMBER_PARSE_FLOAT_PREFIX && (size_t)count != len) return false; + + *out = value; + if (processed) *processed = leading + (size_t)count; + + return true; +} + +static inline size_t copy_truncated_number_result(char *dst, size_t dstlen, const char *src, size_t srclen) { + if (!dst || dstlen == 0) return srclen; + size_t n = srclen < dstlen - 1 ? srclen : dstlen - 1; + + if (n > 0) std::memcpy(dst, src, n); + dst[n] = '\0'; + + return srclen; +} + +template +static size_t ant_format_number( + char *buf, + size_t len, + char *scratch, + size_t scratch_len, + Format format +) { + char *out = (buf && len >= scratch_len) ? buf : scratch; + size_t out_len = (out == buf) ? len : scratch_len; + + StringBuilder builder(out, (int)out_len); + bool ok = format(&builder); + if (!ok) return 0; + + int pos = builder.position(); + if (pos < 0) return 0; + builder.Finalize(); + + if (out == buf) return (size_t)pos; + return copy_truncated_number_result(buf, len, scratch, (size_t)pos); +} + +extern "C" size_t ant_number_to_shortest(double value, char *buf, size_t len) { + char scratch[kShortestBufferSize]; + return ant_format_number(buf, len, scratch, sizeof(scratch), [value](StringBuilder *builder) { + return DoubleToStringConverter::EcmaScriptConverter().ToShortest(value, builder); + }); +} + +extern "C" size_t ant_number_to_fixed(double value, int digits, char *buf, size_t len) { + char scratch[kFixedBufferSize]; + return ant_format_number(buf, len, scratch, sizeof(scratch), [value, digits](StringBuilder *builder) { + return DoubleToStringConverter::EcmaScriptConverter().ToFixed(value, digits, builder); + }); +} + +extern "C" size_t ant_number_to_precision(double value, int precision, char *buf, size_t len) { + char scratch[kPrecisionBufferSize]; + return ant_format_number(buf, len, scratch, sizeof(scratch), [value, precision](StringBuilder *builder) { + return DoubleToStringConverter::EcmaScriptConverter().ToPrecision(value, precision, builder); + }); +} + +extern "C" size_t ant_number_to_exponential(double value, int digits, char *buf, size_t len) { + char scratch[kExponentialBufferSize]; + return ant_format_number(buf, len, scratch, sizeof(scratch), [value, digits](StringBuilder *builder) { + return DoubleToStringConverter::EcmaScriptConverter().ToExponential(value, digits, builder); + }); +} diff --git a/src/silver/lexer.c b/src/silver/lexer.c index 7e411f3..29d45fc 100644 --- a/src/silver/lexer.c +++ b/src/silver/lexer.c @@ -5,9 +5,10 @@ #include "tokens.h" #include "utf8.h" #include "errors.h" +#include "numbers.h" #include -#include +#include #include void sv_lexer_init(sv_lexer_t *lx, ant_t *js, const char *code, ant_offset_t clen, bool strict) { @@ -561,59 +562,88 @@ slow_path_loop:; return sv_parsekeyword(buf, *tlen); } -static inline ant_offset_t parse_decimal(const char *buf, ant_offset_t maxlen, double *out) { - uint64_t int_part = 0, frac_part = 0; - int frac_digits = 0; - ant_offset_t i = 0; +static inline bool is_decimal_literal_char(char ch) { + return IS_DIGIT(ch) || ch == '_'; +} - while (i < maxlen && (IS_DIGIT(buf[i]) || buf[i] == '_')) { - if (buf[i] != '_') int_part = int_part * 10 + (buf[i] - '0'); - i++; +static inline bool has_numeric_separator(const char *src, ant_offset_t len) { + return memchr(src, '_', (size_t)len) != NULL; +} + +static inline const char *scan_decimal_literal_chars(const char *p, const char *end) { + while (p < end && is_decimal_literal_char(*p)) p++; + return p; +} + +static inline ant_offset_t scan_decimal_literal(const char *buf, ant_offset_t maxlen) { + const char *start = buf; + const char *end = buf + maxlen; + const char *p = scan_decimal_literal_chars(buf, end); + + if (p < end && *p == '.') { + p = scan_decimal_literal_chars(p + 1, end); } - if (i < maxlen && buf[i] == '.') { - i++; - while (i < maxlen && (IS_DIGIT(buf[i]) || buf[i] == '_')) { - if (buf[i] != '_') { frac_part = frac_part * 10 + (buf[i] - '0'); frac_digits++; } - i++; - } + if (p < end && ((*p | 0x20) == 'e')) { + p++; + if (p < end && (*p == '+' || *p == '-')) p++; + p = scan_decimal_literal_chars(p, end); } - static const double neg_pow10[] = { - 1e0,1e-1,1e-2,1e-3,1e-4,1e-5,1e-6,1e-7,1e-8,1e-9,1e-10, - 1e-11,1e-12,1e-13,1e-14,1e-15,1e-16,1e-17,1e-18,1e-19,1e-20 - }; - - static const double pos_pow10[] = { - 1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10, - 1e11,1e12,1e13,1e14,1e15,1e16,1e17,1e18,1e19,1e20 - }; + return (ant_offset_t)(p - start); +} - double val = (double)int_part; - if (frac_digits > 0) { - val += (frac_digits <= 20) - ? (double)frac_part * neg_pow10[frac_digits] - : (double)frac_part * pow(10.0, -frac_digits); +static inline size_t copy_without_numeric_separators(const char *src, ant_offset_t len, char *dst) { + const char *end = src + len; + char *start = dst; + + while (src < end) { + char ch = *src++; + if (ch != '_') *dst++ = ch; } + + return (size_t)(dst - start); +} - if (i < maxlen && (buf[i] == 'e' || buf[i] == 'E')) { - i++; - int exp_sign = 1, exp_val = 0; - if (i < maxlen && (buf[i] == '+' || buf[i] == '-')) { - exp_sign = (buf[i] == '-') ? -1 : 1; - i++; - } - while (i < maxlen && (IS_DIGIT(buf[i]) || buf[i] == '_')) { - if (buf[i] != '_') exp_val = exp_val * 10 + (buf[i] - '0'); - i++; +typedef struct { + ant_offset_t len; + double value; + bool ok; +} decimal_literal_t; + +static inline decimal_literal_t parse_decimal_literal(const char *buf, ant_offset_t maxlen) { + ant_offset_t toklen = scan_decimal_literal(buf, maxlen); + const char *digits = buf; + + size_t digits_len = (size_t)toklen; + char stack_buf[128]; + char *clean = NULL; + + decimal_literal_t result = { + .len = toklen, + .value = 0.0, + .ok = false + }; + + if (has_numeric_separator(buf, toklen)) { + clean = stack_buf; + if (toklen > (ant_offset_t)sizeof(stack_buf)) { + clean = malloc((size_t)toklen); + if (!clean) return result; } - if (exp_val <= 20) { - val = (exp_sign > 0) ? val * pos_pow10[exp_val] : val * neg_pow10[exp_val]; - } else val *= pow(10.0, exp_sign * exp_val); + + digits_len = copy_without_numeric_separators(buf, toklen, clean); + digits = clean; } - *out = val; - return i; + result.ok = ant_number_parse( + digits, digits_len, + ANT_NUMBER_PARSE_DECIMAL, + &result.value, NULL + ); + + if (clean && clean != stack_buf) free(clean); + return result; } static inline ant_offset_t parse_binary(const char *buf, ant_offset_t maxlen, double *out) { @@ -691,8 +721,26 @@ static inline uint8_t parse_number(sv_lexer_t *lx, const char *buf, ant_offset_t lx->st.tok = TOK_ERR; lx->st.tlen = 1; return TOK_ERR; - } else numlen = parse_decimal(buf, remaining, &value); - } else numlen = parse_decimal(buf, remaining, &value); + } else { + decimal_literal_t literal = parse_decimal_literal(buf, remaining); + numlen = literal.len; + if (!literal.ok) { + lx->st.tok = TOK_ERR; + lx->st.tlen = numlen; + return TOK_ERR; + } + value = literal.value; + } + } else { + decimal_literal_t literal = parse_decimal_literal(buf, remaining); + numlen = literal.len; + if (!literal.ok) { + lx->st.tok = TOK_ERR; + lx->st.tlen = numlen; + return TOK_ERR; + } + value = literal.value; + } lx->st.tval = tov(value); ant_offset_t toklen = numlen; @@ -1042,18 +1090,19 @@ static inline uint8_t parse_operator(sv_lexer_t *lx, const char *buf, ant_offset case '.': if (MATCH3('.','.', '.')) { lx->st.tok = TOK_REST; lx->st.tlen = 3; } else if (rem > 1 && IS_DIGIT(buf[1])) { - double val; - ant_offset_t numlen = parse_decimal(buf, rem, &val); - if (number_literal_has_invalid_tail(buf, rem, numlen)) { + decimal_literal_t literal = parse_decimal_literal(buf, rem); + if (!literal.ok || number_literal_has_invalid_tail(buf, rem, literal.len)) { lx->st.tok = TOK_ERR; - lx->st.tlen = numlen; + lx->st.tlen = literal.len; } else { - lx->st.tlen = numlen; - lx->st.tval = tov(val); + lx->st.tlen = literal.len; + lx->st.tval = tov(literal.value); lx->st.tok = TOK_NUMBER; } + } else { + lx->st.tok = TOK_DOT; + lx->st.tlen = 1; } - else { lx->st.tok = TOK_DOT; lx->st.tlen = 1; } break; default: diff --git a/tests/bench_number_conversion.js b/tests/bench_number_conversion.js new file mode 100644 index 0000000..3df07c1 --- /dev/null +++ b/tests/bench_number_conversion.js @@ -0,0 +1,146 @@ +const now = + typeof performance !== "undefined" && performance && typeof performance.now === "function" + ? () => performance.now() + : () => Date.now(); + +const scale = + typeof process !== "undefined" && process.argv && process.argv.length > 2 + ? Math.max(1, Number(process.argv[2]) || 1) + : 1; + +let sink = 0; + +function mixNumber(value) { + sink = (sink + (value * 1000003) | 0) ^ (sink << 5); +} + +function mixString(value) { + sink = (sink + value.length * 33 + value.charCodeAt(value.length - 1)) | 0; +} + +function bench(name, iterations, fn) { + const warmup = Math.max(1, iterations >> 4); + fn(warmup); + + const start = now(); + const ops = fn(iterations); + const elapsed = now() - start; + const nsPerOp = elapsed > 0 ? (elapsed * 1e6) / ops : 0; + const opsPerSec = elapsed > 0 ? (ops * 1000) / elapsed : 0; + + console.log( + name + + ": " + + elapsed.toFixed(2) + + "ms (" + + ops + + " ops, " + + nsPerOp.toFixed(2) + + " ns/op, " + + opsPerSec.toFixed(0) + + " ops/s)" + ); +} + +const decimalStrings = [ + "0.7875", + "2.675", + "123456789.125", + "-0.0000033333333333333333", + "1.7976931348623157e308", + "5e-324", + " 42.5 ", + "0x10", + "0b101010", + "0o755", +]; + +const floatStrings = [ + "0.7875px", + "2.675 and change", + " Infinity!", + "-0.0000033333333333333333ms", + "123456789.125;", + "5e-324end", +]; + +const literalSources = [ + "0.7875", + "2.675", + "123456789.125", + "-0.0000033333333333333333", + "1.7976931348623157e308", + "5e-324", + "1_234_567.8_9", + "1.e+1", +]; + +const numbers = [ + 0.7875, + 0.7876, + 2.675, + 123456789.125, + -0.0000033333333333333333, + 1.7976931348623157e308, + 5e-324, + Math.PI, +]; + +console.log("Number conversion benchmark (scale " + scale + ")"); + +bench("Number(string)", 500000 * scale, n => { + let sum = 0; + for (let i = 0; i < n; i++) { + sum += Number(decimalStrings[i % decimalStrings.length]); + } + mixNumber(sum); + return n; +}); + +bench("parseFloat(prefix)", 500000 * scale, n => { + let sum = 0; + for (let i = 0; i < n; i++) { + sum += parseFloat(floatStrings[i % floatStrings.length]); + } + mixNumber(sum); + return n; +}); + +bench("eval(decimal literal)", 60000 * scale, n => { + let sum = 0; + for (let i = 0; i < n; i++) { + sum += eval(literalSources[i % literalSources.length]); + } + mixNumber(sum); + return n; +}); + +bench("String(number)", 500000 * scale, n => { + for (let i = 0; i < n; i++) { + mixString(String(numbers[i & 7])); + } + return n; +}); + +bench("number.toFixed(20)", 250000 * scale, n => { + for (let i = 0; i < n; i++) { + mixString(numbers[i & 7].toFixed(20)); + } + return n; +}); + +bench("number.toPrecision(17)", 250000 * scale, n => { + for (let i = 0; i < n; i++) { + mixString(numbers[i & 7].toPrecision(17)); + } + return n; +}); + +bench("number.toExponential(20)", 250000 * scale, n => { + for (let i = 0; i < n; i++) { + mixString(numbers[i & 7].toExponential(20)); + } + return n; +}); + +console.log("sink:", sink); diff --git a/vendor/double-conversion.wrap b/vendor/double-conversion.wrap new file mode 100644 index 0000000..c00dae5 --- /dev/null +++ b/vendor/double-conversion.wrap @@ -0,0 +1,9 @@ +[wrap-file] +directory = double-conversion-3.4.0 +source_url = https://github.com/google/double-conversion/archive/refs/tags/v3.4.0.tar.gz +source_filename = double-conversion-3.4.0.tar.gz +source_hash = 42fd4d980ea86426e457b24bdfa835a6f5ad9517ddb01cdb42b99ab9c8dd5dc9 +patch_directory = double-conversion + +[provide] +double-conversion = double_conversion_dep diff --git a/vendor/packagefiles/double-conversion/meson.build b/vendor/packagefiles/double-conversion/meson.build new file mode 100644 index 0000000..3360022 --- /dev/null +++ b/vendor/packagefiles/double-conversion/meson.build @@ -0,0 +1,30 @@ +project('double-conversion', 'cpp', + default_options: [ + 'cpp_std=c++17', + 'warning_level=0', + 'default_library=static', + ] +) + +double_conversion_inc = include_directories('.') + +double_conversion_lib = static_library( + 'double-conversion', + files( + 'double-conversion/bignum.cc', + 'double-conversion/bignum-dtoa.cc', + 'double-conversion/cached-powers.cc', + 'double-conversion/double-to-string.cc', + 'double-conversion/fast-dtoa.cc', + 'double-conversion/fixed-dtoa.cc', + 'double-conversion/string-to-double.cc', + 'double-conversion/strtod.cc', + ), + include_directories: double_conversion_inc, + install: false, +) + +double_conversion_dep = declare_dependency( + link_with: double_conversion_lib, + include_directories: double_conversion_inc, +)