diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 09e080cdf..6ea6c4c3a 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -555,6 +555,21 @@ static void scr_sidx_extend_to_u16(const ScrStr *s, ScrSidx *e, size_t u16) { scr_sidx_finish(s, e); } +/* A large, not-yet-complete string can have a long proven-ASCII prefix + * before its first non-ASCII byte. That prefix is an exact identity map, but + * merely advancing indexed_{cu,cb} through it leaves alternating on-demand + * lookups with only the hot cursor and therefore linear backtracks. Once an + * indexed conversion reaches such a prefix, retain its arithmetic stride + * anchors too. A later full scan still drops them if the whole string proves + * ASCII, so the all-ASCII steady state remains the allocation-free identity + * fast path. */ +static void scr_sidx_materialize_identity_prefix(const ScrStr *s, ScrSidx *e) { + if (e->npoints == 0 && e->indexed_cb != 0 && + e->indexed_cb == e->indexed_cu) { + (void)scr_sidx_prepare_points(s, e); + } +} + /* Cached UTF-16 length. Large strings extend from their exact previously * indexed prefix, retaining sparse start/end anchors; short strings keep the * historical single word-wise scan. `u16len == byte len` proves ASCII and @@ -660,6 +675,7 @@ static size_t scr_u16_to_byte_c(const ScrStr *s, ScrSidx *e, size_t u16, return u16 < s->len ? u16 : s->len; } scr_sidx_extend_to_u16(s, e, u16); + scr_sidx_materialize_identity_prefix(s, e); ScrSidxPoint near = scr_sidx_near_u16(s, e, u16); size_t cu = near.cu, cb = near.cb; while (cu > u16) scr_sidx_back(s, &cu, &cb); @@ -692,6 +708,7 @@ static size_t scr_byte_to_u16_c(const ScrStr *s, ScrSidx *e, while (e->indexed_cb < byte_off) scr_sidx_extend_one(s, e); scr_sidx_finish(s, e); } + scr_sidx_materialize_identity_prefix(s, e); ScrSidxPoint near = scr_sidx_near_byte(s, e, byte_off); size_t cu = near.cu, cb = near.cb; while (cb > byte_off) scr_sidx_back(s, &cu, &cb); diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index 6f1354a85..a14d7cb13 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -341,6 +341,38 @@ static void sparse_index_asserts(void) { scr_str_release(s); scr_sidx_test_reset_cache(); } + +/* Do not wait for the first non-ASCII byte before proving this access shape. + * A string can have megabytes of ordinary ASCII followed by one emoji: no + * `.length` prime is involved here, and alternating distant reads in that + * prefix must retain identity checkpoints instead of repeatedly walking the + * distance between the two hot-cursor positions. */ +static void sparse_ascii_prefix_asserts(void) { + enum { PREFIX = 4 * 1024 * 1024, QUERIES = 8 }; + const size_t first = (size_t)1024 * 1024 + 137; + const size_t second = (size_t)3 * 1024 * 1024 + 271; + char *raw = malloc((size_t)PREFIX + 4); + if (!raw) { sidx_fail("ASCII-prefix test allocation"); return; } + memset(raw, 'a', PREFIX); + memcpy(raw + PREFIX, "\xF0\x9F\x98\x80", 4); /* terminal emoji */ + ScrStr *s = scr_str_new(raw, (size_t)PREFIX + 4); + free(raw); + + scr_sidx_test_reset_cache(); + scr_sidx_test_reset_steps(); + for (size_t q = 0; q < QUERIES; q++) { + size_t at = q & 1 ? second : first; + if (scr_str_char_code_at(s, (double)at) != 97.0) + sidx_fail("ASCII-prefix charCodeAt result"); + } + if (scr_sidx_test_points() == 0) + sidx_fail("ASCII-prefix identity checkpoints were not retained"); + if (scr_sidx_test_walk_steps() > (size_t)QUERIES * 4200) + sidx_fail("ASCII-prefix lookup exceeded sparse stride bound"); + + scr_str_release(s); + scr_sidx_test_reset_cache(); +} #endif int main(int argc, char **argv) { @@ -507,6 +539,7 @@ int main(int argc, char **argv) { accumulation_asserts(); #ifdef SCR_SIDX_TEST sparse_index_asserts(); + sparse_ascii_prefix_asserts(); #endif #ifdef SCR_RC_AUDIT