From abb87f4dd73ef744c977a2973e944516ee1695b4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 21:13:01 -0500 Subject: [PATCH] Grow the ASCII run before walking for forward index reads A scanner reading a mostly ASCII text front to back (slices and positioned startsWith at increasing positions) walked from the hot cursor on every read within 64 units of it, since the near-cursor walk came before the ASCII-run growth. A forward read past the run now grows the run first. When a non-ASCII character ends the run, a nearby read walks from the run's end or the cursor and starts a new run where it lands. --- packages/runtime/src/scr_string.c | 36 +++++++++++++++++---- packages/runtime/test/test_string.c | 49 +++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index a9138cedc..d712ebb84 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1756,6 +1756,36 @@ static __attribute__((noinline)) size_t scr_u16_to_byte_walk(const ScrStr *s, Sc e->cb = cb; return cb; } + /* A forward access past the cached ASCII run (a scanner taking slices at + * increasing positions of a mostly ASCII text) grows the run over the + * ASCII bytes up to it. When a non-ASCII character ends the run first, a + * nearby access walks from the run's end or the hot cursor, whichever is + * nearer, and a new run starts where the walk lands, so the accesses after + * it map by arithmetic again. */ + if (u16 >= e->ascii_cu) { + if (scr_sidx_ascii_grow(s, e, e->ascii_cb + (u16 - e->ascii_cu))) { + e->cb = e->ascii_cb + (u16 - e->ascii_cu); + e->cu = u16; + *mid = false; + return e->cb; + } + size_t run_cu = e->ascii_cu + (e->ascii_end - e->ascii_cb); + if (e->ascii_end < s->len && u16 - run_cu <= SCR_SIDX_NEAR_UNITS) { + size_t cu = run_cu, cb = e->ascii_end; + if (e->cb <= s->len && e->cu >= cu && e->cu <= u16) { + cu = e->cu; + cb = e->cb; + } + size_t at = scr_sidx_walk_from(s, e, cu, cb, u16, mid, false); + if (!*mid && at < s->len && (unsigned char)s->data[at] < 0x80) { + e->ascii_cu = e->cu; + e->ascii_cb = at; + e->ascii_end = at; + (void)scr_sidx_ascii_grow(s, e, at); + } + return at; + } + } /* Within a short distance of the hot cursor (a scanner * taking slices near its position), walk from that cursor: no index * search, extension or ASCII-run growth is cheaper. */ @@ -1785,12 +1815,6 @@ static __attribute__((noinline)) size_t scr_u16_to_byte_walk(const ScrStr *s, Sc return cb; } } - if (u16 >= e->ascii_cu && scr_sidx_ascii_grow(s, e, e->ascii_cb + (u16 - e->ascii_cu))) { - e->cb = e->ascii_cb + (u16 - e->ascii_cu); - e->cu = u16; - *mid = false; - return e->cb; - } scr_sidx_extend_to_u16(s, e, u16); /* Extending a far-end lookup can just have proved identity. Do not * materialize an index that the identity fast path will never consult. */ diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index 5a3bdfe5f..3bce02761 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -927,6 +927,54 @@ static void local_navigation_asserts(void) { scr_sidx_test_reset_cache(); } +/* A scanner reading a mostly ASCII text front to back (positioned + * startsWith and substrings at increasing positions, with a non-ASCII + * character every 300 bytes) maps its positions through the ASCII run, so + * it walks only across the non-ASCII characters. */ +static void forward_scan_asserts(void) { + enum { ASCII = 300, UNITS = ASCII + 1, REPS = 100 }; + char piece[ASCII + 2]; + for (size_t i = 0; i < ASCII; i++) piece[i] = (char)('a' + i % 26); + memcpy(piece + ASCII, "\xC3\xA9", 2); /* é: one unit */ + ScrStr *unit = scr_str_new(piece, sizeof piece); + ScrStr *s = scr_str_repeat(unit, REPS); + scr_str_release(unit); + scr_sidx_test_reset_cache(); + if (scr_str_utf16_len(s) != UNITS * REPS) sidx_fail("forward scan length"); + ScrStr *needle = scr_str_new("ab", 2); + scr_sidx_test_reset_steps(); + for (size_t at = 0; at + 2 <= UNITS * REPS; at += 5) { + size_t offset = at % UNITS; + bool expected = offset + 1 < ASCII && offset % 26 == 0; + if (scr_str_starts_with_from(s, needle, (double)at) != expected) { + sidx_fail("forward startsWith"); + break; + } + char want[4]; + size_t n = 0; + for (size_t k = at; k < at + 2; k++) { + size_t o = k % UNITS; + if (o < ASCII) want[n++] = (char)('a' + o % 26); + else { + want[n++] = '\xC3'; + want[n++] = '\xA9'; + } + } + ScrStr *part = scr_str_substring(s, (double)at, (double)(at + 2)); + bool same = part->len == n && memcmp(part->data, want, n) == 0; + scr_str_release(part); + if (!same) { + sidx_fail("forward substring"); + break; + } + } + if (scr_sidx_test_walk_steps() > REPS * 16) + sidx_fail("forward reads walked between non-ASCII characters"); + scr_str_release(needle); + scr_str_release(s); + scr_sidx_test_reset_cache(); +} + /* An entry borrowed for a receiver must remain its entry when a second * string needs indexing. Fill the cursor tier so the needle would evict * precisely the receiver under a receiver-first lookup order. */ @@ -1244,6 +1292,7 @@ int main(int argc, char **argv) { index_activity_asserts(); sparse_slice_churn_asserts(); local_navigation_asserts(); + forward_scan_asserts(); positioned_suffix_eviction_asserts(); #endif