Skip to content

Commit 786a322

Browse files
committed
Improve: UTF-8 equivalence checks
1 parent dd4c4b0 commit 786a322

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

scripts/test_stringzilla.cpp

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,149 @@ void test_sha256_equivalence(
387387
}
388388
}
389389

390+
/**
391+
* @brief Tests UTF-8 functions across different SIMD backends against the serial implementation.
392+
*
393+
* Generates random strings containing:
394+
* - ASCII content (1-byte)
395+
* - Multi-byte UTF-8 characters (2, 3, 4-byte) - correct and broken ones
396+
* - All 25 Unicode White_Space characters (including all newlines) + CRLF sequences - correct and partial ones
397+
*
398+
* For each generated string, compares:
399+
* - sz_utf8_count: character counting
400+
* - sz_utf8_find_newline: newline detection (position and matched length)
401+
* - sz_utf8_find_whitespace: whitespace detection (position and matched length)
402+
*/
403+
void test_utf8_equivalence( //
404+
sz_utf8_count_t count_base, sz_utf8_count_t count_simd, //
405+
sz_utf8_find_boundary_t newline_base, //
406+
sz_utf8_find_boundary_t newline_simd, //
407+
sz_utf8_find_boundary_t whitespace_base, //
408+
sz_utf8_find_boundary_t whitespace_simd, //
409+
std::size_t min_text_lentgh = 4000, std::size_t min_iterations = 10000) {
410+
411+
auto check = [&](std::string const &text) {
412+
sz_cptr_t data = text.data();
413+
sz_size_t len = text.size();
414+
415+
// Test `sz_utf8_count` equivalence
416+
sz_size_t count_result_base = count_base(data, len);
417+
sz_size_t count_result_simd = count_simd(data, len);
418+
assert(count_result_base == count_result_simd);
419+
420+
// Test `sz_utf8_find_newline` equivalence by scanning the entire string
421+
sz_cptr_t pos = data;
422+
sz_size_t remaining = len;
423+
while (remaining > 0) {
424+
sz_size_t matched_base = 0, matched_simd = 0;
425+
sz_cptr_t found_base = newline_base(pos, remaining, &matched_base);
426+
sz_cptr_t found_simd = newline_simd(pos, remaining, &matched_simd);
427+
assert(found_base == found_simd && "Mismatch in newline detection");
428+
if (found_base == SZ_NULL_CHAR) break;
429+
assert(matched_base == matched_simd);
430+
sz_size_t offset = (found_base - pos) + matched_base;
431+
pos += offset;
432+
remaining -= offset;
433+
}
434+
435+
// Test `sz_utf8_find_whitespace` equivalence by scanning the entire string
436+
pos = data;
437+
remaining = len;
438+
while (remaining > 0) {
439+
sz_size_t matched_base = 0, matched_simd = 0;
440+
sz_cptr_t found_base = whitespace_base(pos, remaining, &matched_base);
441+
sz_cptr_t found_simd = whitespace_simd(pos, remaining, &matched_simd);
442+
assert(found_base == found_simd && "Mismatched position in whitespace detection");
443+
if (found_base == SZ_NULL_CHAR) break;
444+
assert(matched_base == matched_simd);
445+
sz_size_t offset = (found_base - pos) + matched_base;
446+
pos += offset;
447+
remaining -= offset;
448+
}
449+
};
450+
451+
// Strings that shouldn't affect the control flow
452+
static char const *utf8_content[] = {
453+
// Various ASCII strings
454+
"",
455+
"a",
456+
"hello",
457+
"012",
458+
"3456789",
459+
// 2-byte Cyrillic П (U+041F), Armenian Ս (U+054D), and Greek Pi π (U+03C0)
460+
"\xD0\x9F",
461+
"\xD5\xA5",
462+
"\xCF\x80",
463+
// 3-byte characters
464+
"\xE0\xA4\xB9", // Hindi ह (U+0939)
465+
"\xE1\x88\xB4", // Ethiopic ሴ (U+1234)
466+
"\xE2\x9C\x94", // Check mark ✔ (U+2714)
467+
// 4-byte emojis: U+1F600 (😀), U+1F601 (😁), U+1F602 (😂)
468+
"\xF0\x9F\x98\x80",
469+
"\xF0\x9F\x98\x81",
470+
"\xF0\x9F\x98\x82",
471+
// Characters with bytes in 0x80-0x8F range (tests unsigned comparison in SIMD)
472+
"\xE2\x82\x80", // U+2080 SUBSCRIPT ZERO (has 0x80 suffix, NOT whitespace)
473+
"\xE2\x84\x8A", // U+210A SCRIPT SMALL G (has 0x8A like HAIR SPACE suffix)
474+
"\xE2\x84\x8D", // U+210D DOUBLE-STRUCK H (has 0x8D suffix)
475+
// Near-miss characters (same prefix as whitespace but different suffix)
476+
"\xE2\x80\xB0", // U+2030 PER MILLE SIGN (E2 80 prefix like whitespace range)
477+
"\xE2\x80\xBB", // U+203B REFERENCE MARK (E2 80 prefix)
478+
"\xE2\x81\xA0", // U+2060 WORD JOINER (E2 81 prefix like MMSP)
479+
"\xE3\x80\x81", // U+3001 IDEOGRAPHIC COMMA (E3 80 prefix like IDEOGRAPHIC SPACE)
480+
"\xE3\x80\x82", // U+3002 IDEOGRAPHIC FULL STOP
481+
// More 4-byte sequences for boundary handling
482+
"\xF0\x9F\x8E\x89", // U+1F389 PARTY POPPER 🎉
483+
"\xF0\x9F\x92\xA9", // U+1F4A9 PILE OF POO 💩
484+
};
485+
486+
// Special characters that will affect control flow
487+
static char const *special_chars[26] = {
488+
"\x09", "\x0A", "\x0B", "\x0C", "\x0D", " ", // 1-byte (6)
489+
"\xC2\x85", "\xC2\xA0", "\r\n", // 2-byte (2)
490+
"\xE1\x9A\x80", "\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", // 3-byte
491+
"\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", "\xE2\x80\x88", // 3-byte
492+
"\xE2\x80\x89", "\xE2\x80\x8A", "\xE2\x80\xA8", "\xE2\x80\xA9", "\xE2\x80\xAF", // 3-byte
493+
"\xE2\x81\x9F", "\xE3\x80\x80", // 3-byte
494+
};
495+
496+
auto &rng = global_random_generator();
497+
std::size_t const utf8_content_count = sizeof(utf8_content) / sizeof(utf8_content[0]);
498+
std::size_t const spaecial_chars_count = sizeof(special_chars) / sizeof(special_chars[0]);
499+
std::size_t const total_strings_to_sample = utf8_content_count + spaecial_chars_count;
500+
std::uniform_int_distribution<std::size_t> content_dist(0, total_strings_to_sample - 1);
501+
502+
// Generate and test many random strings
503+
for (std::size_t iteration = 0; iteration < min_iterations; ++iteration) {
504+
std::string text;
505+
506+
// Build up a random string of at least `min_text_length` bytes
507+
while (text.size() < min_text_lentgh) {
508+
std::size_t random_content_index = content_dist(rng);
509+
if (random_content_index < utf8_content_count) { text.append(utf8_content[random_content_index]); }
510+
else { text.append(special_chars[random_content_index - utf8_content_count]); }
511+
}
512+
check(text);
513+
514+
// Now, let's replace 10% of bytes in the sequence with a NUL character, thus breaking many valid codepoints
515+
std::size_t num_bytes_to_corrupt = text.size() / 10;
516+
std::uniform_int_distribution<std::size_t> pos_dist(0, text.size() - 1);
517+
for (std::size_t i = 0; i < num_bytes_to_corrupt; ++i) {
518+
std::size_t pos = pos_dist(rng);
519+
text[pos] = '\0';
520+
}
521+
check(text);
522+
523+
// Swap 10% of bytes at random positions, creating malformed UTF-8 sequences
524+
for (std::size_t i = 0; i < num_bytes_to_corrupt; ++i) {
525+
std::size_t pos1 = pos_dist(rng);
526+
std::size_t pos2 = pos_dist(rng);
527+
std::swap(text[pos1], text[pos2]);
528+
}
529+
check(text);
530+
}
531+
}
532+
390533
void test_equivalence() {
391534

392535
// Ensure the seed affects hash results
@@ -453,6 +596,40 @@ void test_equivalence() {
453596
sz_sha256_state_init_neon, sz_sha256_state_update_neon, sz_sha256_state_digest_neon //
454597
);
455598
#endif
599+
600+
// Test UTF-8 functions
601+
#if SZ_USE_HASWELL
602+
test_utf8_equivalence( //
603+
sz_utf8_count_serial, sz_utf8_count_haswell, //
604+
sz_utf8_find_newline_serial, //
605+
sz_utf8_find_newline_haswell, //
606+
sz_utf8_find_whitespace_serial, //
607+
sz_utf8_find_whitespace_haswell);
608+
#endif
609+
#if SZ_USE_ICE
610+
test_utf8_equivalence( //
611+
sz_utf8_count_serial, sz_utf8_count_ice, //
612+
sz_utf8_find_newline_serial, //
613+
sz_utf8_find_newline_ice, //
614+
sz_utf8_find_whitespace_serial, //
615+
sz_utf8_find_whitespace_ice);
616+
#endif
617+
#if SZ_USE_NEON
618+
test_utf8_equivalence( //
619+
sz_utf8_count_serial, sz_utf8_count_neon, //
620+
sz_utf8_find_newline_serial, //
621+
sz_utf8_find_newline_neon, //
622+
sz_utf8_find_whitespace_serial, //
623+
sz_utf8_find_whitespace_neon);
624+
#endif
625+
#if SZ_USE_SVE2
626+
test_utf8_equivalence( //
627+
sz_utf8_count_serial, sz_utf8_count_sve2, //
628+
sz_utf8_find_newline_serial, //
629+
sz_utf8_find_newline_sve2, //
630+
sz_utf8_find_whitespace_serial, //
631+
sz_utf8_find_whitespace_sve2);
632+
#endif
456633
};
457634

458635
/**

0 commit comments

Comments
 (0)