@@ -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+
390533void 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