diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 41588f25a..9b21dd722 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1769,14 +1769,9 @@ static inline double scr_to_integer_or_infinity(double x) { return isnan(x) ? 0.0 : x; } -/* Byte substring search (needle and hay are both canonical WTF-8, - * so any byte-level match starts on a char boundary — UTF-8 is - * self-synchronizing). Empty needle matches at hay. */ -static const char *scr_byte_find(const char *hay, size_t hay_len, - const char *nee, size_t nee_len) { - if (nee_len == 0) return hay; - if (nee_len > hay_len) return NULL; - if (nee_len == 1) return memchr(hay, (unsigned char)nee[0], hay_len); +/* scr_byte_find for a needle of at least two bytes, no longer than hay. */ +static __attribute__((noinline)) const char *scr_byte_find_long(const char *hay, size_t hay_len, + const char *nee, size_t nee_len) { const unsigned char first = (unsigned char)nee[0]; const unsigned char second = (unsigned char)nee[1]; /* The last possible match start. */ @@ -1826,6 +1821,18 @@ static const char *scr_byte_find(const char *hay, size_t hay_len, return NULL; } +/* Byte substring search (needle and hay are both canonical WTF-8, + * so any byte-level match starts on a char boundary — UTF-8 is + * self-synchronizing). Empty needle matches at hay. The one-byte case, + * every split on a separator character, goes to memchr without a frame. */ +static inline const char *scr_byte_find(const char *hay, size_t hay_len, + const char *nee, size_t nee_len) { + if (nee_len == 0) return hay; + if (nee_len > hay_len) return NULL; + if (nee_len == 1) return memchr(hay, (unsigned char)nee[0], hay_len); + return scr_byte_find_long(hay, hay_len, nee, nee_len); +} + /* ── searches for a needle that can match half of a surrogate pair ───── * Canonical storage makes byte matches exact for every needle except one * that starts with a lone low surrogate or ends with a lone high one: JS @@ -2020,6 +2027,13 @@ double scr_str_char_code_at(ScrStr *s, double i) { } double scr_str_index_of(ScrStr *s, ScrStr *needle, double fromIndex) { + /* A search from the start of a proven-ASCII receiver: byte offsets are + * UTF-16 indexes, and a needle with other bytes is never found. A start + * index that is not positive (NaN, -0 or negative) clamps to 0. */ + if (s->ascii && !(fromIndex > 0)) { + const char *found = scr_byte_find(s->data, s->len, needle->data, needle->len); + return found ? (double)(found - s->data) : -1.0; + } double pos = scr_to_integer_or_infinity(fromIndex); if (needle->len > s->len) return -1.0; ScrSidx *e = scr_sidx_for(s); @@ -2142,6 +2156,12 @@ static size_t scr_slice_boundary(double v, size_t len16) { } ScrStr *scr_str_slice(ScrStr *s, double start, double end) { + /* A proven-ASCII receiver needs no index map (scr_str_slice_units). */ + if (s->ascii) { + size_t from = scr_slice_boundary(scr_to_integer_or_infinity(start), s->len); + size_t to = scr_slice_boundary(scr_to_integer_or_infinity(end), s->len); + return scr_str_slice_units(s, NULL, from, to); + } ScrSidx *e = scr_sidx_for(s); size_t len16 = scr_sidx_len(s, e); size_t from = scr_slice_boundary(scr_to_integer_or_infinity(start), len16); @@ -2407,8 +2427,10 @@ void scr_str_split_cursor_init(ScrSplitCursor *cursor, uint32_t limit) { cursor->pending_unit = 0; } -static ScrStr *scr_str_split_piece(const char *bytes, size_t len, ScrStr **scratch) { - if (!scratch || len <= 4) return scr_str_from_span(bytes, len); +/* `ascii`: the bytes are a span of a proven-ASCII string. */ +static ScrStr *scr_str_split_piece(const char *bytes, size_t len, ScrStr **scratch, bool ascii) { + if (!scratch || len <= 4) return ascii ? scr_str_from_span_ascii(bytes, len) + : scr_str_from_span(bytes, len); ScrStr *piece = *scratch; if (piece && piece->rc == 1 && piece->cap >= len) { /* The scratch owner is the only remaining reference. Invalidate all @@ -2419,10 +2441,12 @@ static ScrStr *scr_str_split_piece(const char *bytes, size_t len, ScrStr **scrat memcpy(piece->data, bytes, len); piece->len = len; piece->data[len] = '\0'; + piece->ascii = ascii; } else { ScrStr *fresh = scr_str_alloc_raw(len, len < 64 ? 64 : len); memcpy(fresh->data, bytes, len); fresh->data[len] = '\0'; + fresh->ascii = ascii; *scratch = fresh; scr_str_release(piece); piece = fresh; @@ -2480,7 +2504,7 @@ const char *scr_str_split_cursor_span(ScrStr *s, ScrStr *sep, ScrSplitCursor *cu } ScrStr *scr_str_split_materialize(const char *bytes, size_t length, ScrStr **scratch) { - return scr_str_split_piece(bytes, length, scratch); + return scr_str_split_piece(bytes, length, scratch, false); } ScrStr *scr_str_split_cursor_next(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor, @@ -2488,7 +2512,7 @@ ScrStr *scr_str_split_cursor_next(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor size_t len; const char *bytes = scr_str_split_span(s, sep, cursor, sep->len == 0, &len); if (!bytes) return NULL; - return scr_str_split_piece(bytes, len, scratch); + return scr_str_split_piece(bytes, len, scratch, s->ascii); } /* Move a bounded batch of pieces into array storage in one operation. @@ -2513,7 +2537,8 @@ static void scr_str_split_fill(ScrArr *out, ScrStr *s, ScrStr *sep, size_t count = 0, len; const char *bytes; while (count < 64 && (bytes = scr_str_split_span(s, sep, cursor, units, &len))) { - ScrStr *piece = scr_str_from_span(bytes, len); + ScrStr *piece = + s->ascii ? scr_str_from_span_ascii(bytes, len) : scr_str_from_span(bytes, len); slots[count++] = (uint64_t)(uintptr_t)piece; } if (count) scr_arr_push_many(out, count, slots); @@ -2522,18 +2547,43 @@ static void scr_str_split_fill(ScrArr *out, ScrStr *s, ScrStr *sep, } ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit_num) { - uint32_t limit = scr_to_uint32(limit_num); + /* split(sep) passes the default limit, 2^32 - 1. */ + uint32_t limit = limit_num == 4294967295.0 ? UINT32_MAX : scr_to_uint32(limit_num); /* An empty separator yields one piece per UTF-16 unit: size it once. */ size_t pieces = 0; if (sep->len == 0) { pieces = s->ascii ? s->len : scr_sidx_len(s, scr_sidx(s)); if (pieces > limit) pieces = limit; } - ScrArr *out = scr_arr_new(SCR_ELEM_STR, pieces); ScrSplitCursor cursor; scr_str_split_cursor_init(&cursor, limit); - if (sep->len == 0) scr_str_split_fill(out, s, sep, &cursor, true); - else scr_str_split_fill(out, s, sep, &cursor, false); + if (sep->len == 0) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, pieces); + scr_str_split_fill(out, s, sep, &cursor, true); + return out; + } + /* Most splits fit one batch: its count sizes the result, whose slots then + * share the header's block (scr_arr_new_inline). A longer split appends + * the rest batch by batch. */ + uint64_t slots[64]; + size_t count = 0, len; + const char *bytes; + while (count < 64 && (bytes = scr_str_split_span(s, sep, &cursor, false, &len))) + slots[count++] = (uint64_t)(uintptr_t)(s->ascii ? scr_str_from_span_ascii(bytes, len) + : scr_str_from_span(bytes, len)); + ScrArr *out = scr_arr_new_inline(SCR_ELEM_STR, count); + if (count) { + /* A fresh inline array of exactly `count` slots: store the pieces and + * their states directly. */ + uint8_t *states = scr_arr_states_end(out); + for (size_t k = 0; k < count; k++) { + out->data[k] = slots[k]; + *scr_arr_state_in(states, k) = SCR_ARR_VALUE; + } + out->len = (uint32_t)count; + out->filled = (uint32_t)count; + } + if (count == 64) scr_str_split_fill(out, s, sep, &cursor, false); return out; } @@ -3104,7 +3154,7 @@ double scr_string_to_number(ScrStr *s) { return scr_bytes_to_number(s->data, s->len); } -double scr_bytes_to_number(const char *p, size_t len) { +static __attribute__((noinline)) double scr_bytes_to_number_any(const char *p, size_t len) { size_t b = 0, e = len; while (b < e) { unsigned char c = (unsigned char)p[b]; @@ -3164,6 +3214,27 @@ double scr_bytes_to_number(const char *p, size_t len) { return r; } +/* An optional sign and 1 to 15 decimal digits, the common shape of a + * parsed field, is an exact integer below 2^53 with no whitespace to trim + * and no other grammar to apply ("-0" is -0, leading zeros are decimal). + * Everything else takes the full grammar. */ +double scr_bytes_to_number(const char *p, size_t len) { + if (len - 1 < 16) { + size_t i = p[0] == '-' || p[0] == '+'; + if (i < len && len - i <= 15) { + int64_t m = 0; + size_t k = i; + for (; k < len; k++) { + unsigned d = (unsigned)(unsigned char)p[k] - '0'; + if (d > 9) break; + m = m * 10 + d; + } + if (k == len) return p[0] == '-' ? -(double)m : (double)m; + } + } + return scr_bytes_to_number_any(p, len); +} + ScrStr *scr_f64_to_scrstr(double x) { if (x >= 0 && x < 100) { /* -0 prints "0" as well */ unsigned v = (unsigned)x; diff --git a/tests/corpus/6950-string-field-fast-paths.ts b/tests/corpus/6950-string-field-fast-paths.ts new file mode 100644 index 000000000..4e9d52b38 --- /dev/null +++ b/tests/corpus/6950-string-field-fast-paths.ts @@ -0,0 +1,48 @@ +// Field parsing on ASCII text: Number() on digit strings (signs, zeros, +// 15 and 16 digits, whitespace and other grammar), indexOf from the start +// and from other positions, slice bounds, and split results of every +// size, including more pieces than one batch and results that grow. +const numbers = [ + "0", "-0", "+0", "7", "-7", "+42", "007", "-007", "123456789012345", "-123456789012345", + "1234567890123456", "9007199254740993", "-", "+", "", " 12", "12 ", "\t3\n", "1e3", + "1.5", "0x1f", "-0x1f", "12px", "1_000", "Infinity", "-Infinity", "+-1", "--1", +]; +for (const text of numbers) { + const value = Number(text); + console.log(JSON.stringify(text), value, Object.is(value, -0)); +} + +const line = "/route/17?request=123 200 45 3200"; +console.log(line.indexOf("?"), line.indexOf(" "), line.indexOf("x"), line.indexOf("")); +console.log(line.indexOf("?", -5), line.indexOf("?", NaN), line.indexOf("?", 10), line.indexOf(" ", 30)); +console.log(line.indexOf("", 100), line.indexOf("", -1), "".indexOf(""), "".indexOf("a")); +console.log(line.indexOf("é"), "aé?".indexOf("?"), "aé?".indexOf("é")); +console.log(line.slice(0, line.indexOf("?")), line.slice(-4), line.slice(5, 2), line.slice(-100, 3)); +console.log(line.slice(NaN, Infinity) === line, line.slice(2, -2), "héllo".slice(1, 3)); + +const fields = line.split(" "); +console.log(fields.length, fields.join("|")); +fields.push("extra"); +fields[0] = fields[0]!.slice(0, 6); +console.log(fields.length, fields.join("|")); + +const many = Array.from({ length: 150 }, (_, i) => String(i * 7)).join(","); +const pieces = many.split(","); +console.log(pieces.length, pieces[0], pieces[63], pieces[64], pieces[149]); +let sum = 0; +for (const piece of pieces) sum += Number(piece); +console.log(sum); +console.log("a,b,,c,".split(","), ",".split(","), "".split(","), "abc".split(",", 0)); +console.log("a b c d".split(" ", 2), "x--y--z".split("--"), "héllo wörld".split(" ")); + +const rows = "/a?x=1 200 5 10\n/b 404 7 20\n/a?y=2 500 9 30\n"; +const totals = new Map(); +for (const row of rows.split("\n")) { + const parts = row.split(" "); + if (parts.length !== 4) continue; + const raw = parts[0]!; + const query = raw.indexOf("?"); + const route = query < 0 ? raw : raw.slice(0, query); + totals.set(route, (totals.get(route) ?? 0) + Number(parts[1]) + Number(parts[3])); +} +console.log([...totals.entries()].map(([route, total]) => route + "=" + total).join(";")); diff --git a/tests/harness/corpus-costs/6950-string-field-fast-paths.ts.json b/tests/harness/corpus-costs/6950-string-field-fast-paths.ts.json new file mode 100644 index 000000000..62a6540b9 --- /dev/null +++ b/tests/harness/corpus-costs/6950-string-field-fast-paths.ts.json @@ -0,0 +1 @@ +{ "plain": 3899, "san": 5479 }