Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 137 additions & 7 deletions src/bun_core/string/immutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2476,21 +2476,95 @@ impl From<ToUTF16Error> for crate::CrateError {
}
}

/// Result of [`to_latin1_or_utf16_alloc`]: the narrowest representation the
/// decoded code points fit in.
Comment on lines +2479 to +2480

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub enum Utf8Decoded {
/// Every byte was ASCII; the caller already holds the 8-bit form.
Ascii,
/// Every decoded code point is `<= U+00FF`; buffer is one Latin-1 byte per
/// code point.
Comment on lines +2484 to +2485

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Latin1(Vec<u8>),
/// At least one code point `> U+00FF` (or a U+FFFD replacement was
/// emitted); buffer is UTF-16LE.
Comment on lines +2487 to +2488

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Utf16(Vec<u16>),
}

/// Decode UTF-8 into the narrowest JSC string representation: [`Utf8Decoded::Ascii`]
/// when every byte is ASCII, [`Utf8Decoded::Latin1`] when every decoded code
/// point is `<= U+00FF`, else [`Utf8Decoded::Utf16`]. Error semantics match
/// [`to_utf16_alloc`]: with `fail_if_invalid` set invalid UTF-8 yields
/// `Err(InvalidByteSequence)`; otherwise invalid sequences are replaced with
/// U+FFFD and the result is always `Utf16` (U+FFFD is not Latin-1).
///
/// Implementation: a single Highway SIMD pass over the input bytes
/// ([`bun_highway::classify_utf8_width`]) decides the output shape up front.
/// `HasWide` runs the existing simdutf UTF-16 decode unchanged.
/// `Latin1Candidate` runs the same decode and, on success, narrows the code
/// units to one byte each via `highway_copy_u16_to_u8` (valid UTF-8 with every
/// byte `< 0xC4` is exactly the set of code points `<= U+00FF`, so no
/// post-decode scan is needed); a decode error means invalid UTF-8, and the
/// replacement path yields UTF-16. simdutf's own `convert_utf8_to_latin1*` is
/// avoided because its icelake kernel passes `__m512i` by value to a helper
/// that does not inline in the debug-ASAN WebKit build and faults on stack
/// alignment.
Comment on lines +2492 to +2509

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub fn to_latin1_or_utf16_alloc(
bytes: &[u8],
fail_if_invalid: bool,
) -> Result<Utf8Decoded, ToUTF16Error> {
use highway::Utf8Width;
let (first, narrow) = match highway::classify_utf8_width(bytes) {
Utf8Width::Ascii => return Ok(Utf8Decoded::Ascii),
Utf8Width::Latin1Candidate(i) => (i, true),
Utf8Width::HasWide(i) => (i, false),
};

let (utf16, was_valid) = to_utf16_alloc_non_ascii(bytes, first, fail_if_invalid, false)?;
if narrow && was_valid {
let mut out: Vec<u8> = Vec::new();
out.try_reserve_exact(utf16.len())
.map_err(|_| ToUTF16Error::OutOfMemory)?;
// SAFETY: `out` has `>= utf16.len()` bytes of capacity (just reserved);
// the kernel writes exactly `utf16.len()` bytes and never reads `out`.
unsafe {
highway::copy_u16_to_u8_raw(utf16.as_ptr(), utf16.len(), out.as_mut_ptr());
out.set_len(utf16.len());
}
return Ok(Utf8Decoded::Latin1(out));
}
Ok(Utf8Decoded::Utf16(utf16))
}

/// `strings.toUTF16Alloc` — convert UTF-8 → UTF-16LE **iff** `bytes` contains
/// any non-ASCII byte; pure-ASCII inputs return `Ok(None)` (caller keeps the
/// 8-bit form). When `fail_if_invalid` is set, invalid UTF-8 yields
/// `Err(InvalidByteSequence)`; otherwise invalid sequences are replaced with
/// U+FFFD. When `sentinel` is set the result
/// includes a trailing 0 u16.
///
/// Callers that build a JS string from the result should prefer
/// [`to_latin1_or_utf16_alloc`], which adds a Latin-1 tier so JSC's 8-bit fast
/// paths stay available for accented-Latin text.
Comment on lines +2543 to +2546

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub fn to_utf16_alloc(
bytes: &[u8],
fail_if_invalid: bool,
sentinel: bool,
) -> Result<Option<Vec<u16>>, ToUTF16Error> {
let Some(_first) = first_non_ascii(bytes) else {
let Some(first) = first_non_ascii(bytes) else {
return Ok(None);
};
to_utf16_alloc_non_ascii(bytes, first as usize, fail_if_invalid, sentinel).map(|(v, _)| Some(v))
}

/// [`to_utf16_alloc`] for input already known to contain a non-ASCII byte at
/// `first`. Also reports whether simdutf accepted the input as valid UTF-8
/// (callers use this to decide whether a Latin-1 narrowing is sound without
/// re-scanning the output).
Comment on lines +2558 to +2561

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

fn to_utf16_alloc_non_ascii(
bytes: &[u8],
first: usize,
fail_if_invalid: bool,
sentinel: bool,
) -> Result<(Vec<u16>, bool), ToUTF16Error> {
let out_length = simdutf::length::utf16::from::utf8(bytes);
let cap = out_length + if sentinel { 1 } else { 0 };
// Hot path: allocate uninitialised and let simdutf write directly into the
Expand Down Expand Up @@ -2519,7 +2593,7 @@ pub fn to_utf16_alloc(
if sentinel {
out.push(0);
}
return Ok(Some(out));
return Ok((out, true));
}
if fail_if_invalid {
return Err(ToUTF16Error::InvalidByteSequence);
Expand All @@ -2529,23 +2603,25 @@ pub fn to_utf16_alloc(
out.try_reserve(bytes.len() + if sentinel { 1 } else { 0 })
.map_err(|_| ToUTF16Error::OutOfMemory)?;
let mut remaining = bytes;
while let Some(i) = first_non_ascii(remaining) {
let i = i as usize;
let mut i = Some(first as u32);
while let Some(j) = i {
let j = j as usize;
// Copy ASCII prefix as-is (one u16 per byte).
out.extend(remaining[..i].iter().map(|&b| u16::from(b)));
remaining = &remaining[i..];
out.extend(remaining[..j].iter().map(|&b| u16::from(b)));
remaining = &remaining[j..];
// Decode one codepoint via `convert_utf8_bytes_into_utf16` so the
// number/position of U+FFFD emissions stays consistent: advance by
// `replacement.len.max(1)`, not 1.
let replacement = unicode_draft::convert_utf8_bytes_into_utf16(remaining);
remaining = &remaining[(replacement.len as usize).max(1)..];
push_codepoint_utf16(&mut out, replacement.code_point);
i = first_non_ascii(remaining);
}
out.extend(remaining.iter().map(|&b| u16::from(b)));
if sentinel {
out.push(0);
}
Ok(Some(out))
Ok((out, false))
}

/// WTF-8 → UTF-16LE iff `bytes` contains any non-ASCII byte; pure-ASCII inputs return `None`.
Expand Down Expand Up @@ -2643,6 +2719,60 @@ mod tests {
assert!(!super::eql_case_insensitive_ascii(b"Ab", b"a", true));
}

#[test]
fn to_latin1_or_utf16_alloc_tiers() {
use super::{Utf8Decoded, to_latin1_or_utf16_alloc};
assert!(matches!(
to_latin1_or_utf16_alloc(b"hello", false).unwrap(),
Utf8Decoded::Ascii
));
// é = C3 A9 → U+00E9
match to_latin1_or_utf16_alloc(b"caf\xC3\xA9", false).unwrap() {
Utf8Decoded::Latin1(v) => assert_eq!(v, b"caf\xE9"),
_ => panic!("expected Latin1"),
}
// U+00FF = C3 BF (boundary)
match to_latin1_or_utf16_alloc(b"x\xC3\xBFx", false).unwrap() {
Utf8Decoded::Latin1(v) => assert_eq!(v, b"x\xFFx"),
_ => panic!("expected Latin1"),
}
// U+0080 = C2 80 (low boundary)
match to_latin1_or_utf16_alloc(b"\xC2\x80", false).unwrap() {
Utf8Decoded::Latin1(v) => assert_eq!(v, b"\x80"),
_ => panic!("expected Latin1"),
}
// U+0100 = C4 80 (just past Latin-1)
match to_latin1_or_utf16_alloc(b"x\xC4\x80x", false).unwrap() {
Utf8Decoded::Utf16(v) => assert_eq!(v, [b'x' as u16, 0x0100, b'x' as u16]),
_ => panic!("expected Utf16"),
}
// Overlong C0 80 passes the < 0xC4 gate but is invalid; must fall through.
match to_latin1_or_utf16_alloc(b"a\xC0\x80b", false).unwrap() {
Utf8Decoded::Utf16(v) => assert_eq!(v, [b'a' as u16, 0xFFFD, 0xFFFD, b'b' as u16]),
_ => panic!("expected Utf16"),
}
// Stray continuation byte.
match to_latin1_or_utf16_alloc(b"a\xA9b", false).unwrap() {
Utf8Decoded::Utf16(v) => assert_eq!(v, [b'a' as u16, 0xFFFD, b'b' as u16]),
_ => panic!("expected Utf16"),
}
// Truncated C3.
match to_latin1_or_utf16_alloc(b"a\xC3", false).unwrap() {
Utf8Decoded::Utf16(v) => assert_eq!(v, [b'a' as u16, 0xFFFD]),
_ => panic!("expected Utf16"),
}
// C3 followed by non-continuation.
match to_latin1_or_utf16_alloc(b"a\xC3\x41b", false).unwrap() {
Utf8Decoded::Utf16(v) => assert_eq!(v, [b'a' as u16, 0xFFFD, b'A' as u16, b'b' as u16]),
_ => panic!("expected Utf16"),
}
// fail_if_invalid propagates through the Latin-1 candidate fall-through.
assert!(matches!(
to_latin1_or_utf16_alloc(b"a\xC0\x80b", true),
Err(super::ToUTF16Error::InvalidByteSequence)
));
}

#[test]
fn convert_utf8_to_utf16_in_buffer_fallback_rejects_malformed_sequences() {
let mut buf = [0u16; 16];
Expand Down
46 changes: 46 additions & 0 deletions src/highway/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ unsafe extern "C" {

fn highway_index_of_newline_or_non_ascii(haystack: *const u8, haystack_len: usize) -> usize;

fn highway_classify_utf8_width(bytes: *const u8, len: usize) -> usize;

fn highway_index_of_newline_or_non_ascii_or_hash_or_at(
haystack: *const u8,
haystack_len: usize,
Expand Down Expand Up @@ -329,6 +331,50 @@ pub fn index_of_any_char(haystack: &[u8], chars: &[u8]) -> Option<usize> {
Some(result)
}

/// One-pass classifier for `bun_core::strings::to_latin1_or_utf16_alloc`.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Utf8Width {
/// Every byte `< 0x80`.
Ascii,
/// Some byte `>= 0x80` but all `< 0xC4`: when the input is well-formed
/// UTF-8 every code point is `<= U+00FF`. Invalid sequences (overlongs,
/// strays, truncated tails) can still land here, so callers must validate.
/// Carries the index of the first byte `>= 0x80`.
Comment on lines +339 to +342

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Latin1Candidate(usize),
/// Some byte `>= 0xC4`. Carries the index of the first byte `>= 0x80`.
HasWide(usize),
}

/// One-pass classifier for [`Utf8Width`]. SIMD-wide; the ASCII prefix is
/// scanned at one compare per block, then the tail at one compare per block
/// with an early exit on the first byte `>= 0xC4`.
Comment on lines +348 to +350

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

#[inline(always)]
pub fn classify_utf8_width(bytes: &[u8]) -> Utf8Width {
if bytes.is_empty() {
return Utf8Width::Ascii;
}
// SAFETY: bytes.ptr/len describe a valid readable range.
let packed = unsafe { highway_classify_utf8_width(bytes.as_ptr(), bytes.len()) };
let first = packed >> 2;
match packed & 3 {
0 => Utf8Width::Ascii,
1 => Utf8Width::Latin1Candidate(first),
_ => Utf8Width::HasWide(first),
}
}

/// Raw-pointer variant of [`copy_u16_to_u8`] for writing into uninitialised
/// storage.
///
/// # Safety
/// `input` must be valid for `count` reads and `output` for `count` writes,
/// non-overlapping.
Comment on lines +366 to +371

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

#[inline(always)]
pub unsafe fn copy_u16_to_u8_raw(input: *const u16, count: usize, output: *mut u8) {
// SAFETY: caller contract.
unsafe { highway_copy_u16_to_u8(input, count, output) }
}

// `&[u16]` requires
// 2-byte alignment. Callers with unaligned data must go through the raw extern.
#[inline(always)]
Expand Down
10 changes: 10 additions & 0 deletions src/jsc/bindings/BunString.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,16 @@ extern "C" BunString BunString__fromUTF8(const char* bytes, size_t length)
return { .tag = BunStringTag::Dead };
}
RELEASE_ASSERT(simdutf::convert_utf8_to_utf16(bytes, length, ptr.data()) == u16Length);
// Narrow to an 8-bit StringImpl when every decoded code unit is
// <= U+00FF so downstream code keeps JSC's 8-bit fast paths.
Comment on lines +441 to +442

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

if (WTF::charactersAreAllLatin1(std::span<const char16_t>(ptr))) {
std::span<Latin1Character> narrow;
auto latin1Impl = WTF::StringImpl::tryCreateUninitialized(u16Length, narrow);
if (latin1Impl) [[likely]] {
WTF::StringImpl::copyCharacters(narrow, ptr);
return { BunStringTag::WTFStringImpl, { .wtf = latin1Impl.leakRef() } };
}
}
return { BunStringTag::WTFStringImpl, { .wtf = impl.leakRef() } };
}

Expand Down
63 changes: 63 additions & 0 deletions src/jsc/bindings/highway_strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,63 @@
return search_len;
}

// One-pass classifier for UTF-8 decode. The low two bits of the return value
// are the class: 0 = every byte < 0x80 (ASCII), 1 = some byte >= 0x80 but all
// < 0xC4 (every well-formed code point fits in Latin-1), 2 = some byte >= 0xC4.
// The remaining bits carry the index of the first byte >= 0x80 (or search_len
// when the class is 0). Packed into a single scalar return so the FFI boundary
// keeps everything in registers. Phase 1 scans ASCII-only blocks at one
// compare per block; phase 2 continues from the first non-ASCII block at one
// compare per block and early-exits on the first byte >= 0xC4.
Comment on lines +737 to +744

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

size_t ClassifyUTF8WidthImpl(const uint8_t* HWY_RESTRICT start_ptr, size_t search_len)

Check failure on line 745 in src/jsc/bindings/highway_strings.cpp

View check run for this annotation

Claude / Claude Code Review

ClassifyUTF8WidthImpl missing from verify-baseline-static allowlists

The new Highway kernel `ClassifyUTF8WidthImpl` is compiled once per SIMD target (AVX3/SVE/etc.) via `HWY_EXPORT`, but it's not listed in `scripts/verify-baseline-static/allowlist-{x64,x64-windows,aarch64}.txt` — CI's static instruction scan is already failing on this PR (`_ZN3bun10N_SVE2_12821ClassifyUTF8WidthImplEPKhm [SVE]` on aarch64, `_ZN3bun10N_AVX3_SPR21ClassifyUTF8WidthImplEPKhm [AVX, AVX512BW, AVX512F]` on x64). The kernel is properly runtime-gated by `HWY_DYNAMIC_DISPATCH`, so the fix i

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The new Highway kernel ClassifyUTF8WidthImpl is compiled once per SIMD target (AVX3/SVE/etc.) via HWY_EXPORT, but it's not listed in scripts/verify-baseline-static/allowlist-{x64,x64-windows,aarch64}.txt — CI's static instruction scan is already failing on this PR (_ZN3bun10N_SVE2_12821ClassifyUTF8WidthImplEPKhm [SVE] on aarch64, _ZN3bun10N_AVX3_SPR21ClassifyUTF8WidthImplEPKhm [AVX, AVX512BW, AVX512F] on x64). The kernel is properly runtime-gated by HWY_DYNAMIC_DISPATCH, so the fix is to add the per-target mangled symbols to all three allowlist files alongside the existing highway_strings.cpp entries (e.g. next to IndexOfNewlineOrNonASCIIImpl).

Extended reasoning...

What the bug is

src/jsc/bindings/highway_strings.cpp gains a new SIMD kernel ClassifyUTF8WidthImpl that is exported via HWY_EXPORT(ClassifyUTF8WidthImpl) and dispatched via HWY_DYNAMIC_DISPATCH. Highway's foreach_target.h mechanism compiles this function once per enabled ISA target — on x64 that's N_AVX2, N_AVX3, N_AVX3_DL, N_AVX3_ZEN4, N_AVX3_SPR, N_AVX10_2; on aarch64 that's N_SVE, N_SVE2, N_SVE_256, N_SVE2_128. Each of those per-target compilations emits post-baseline instructions (AVX-512, SVE) into the binary.

Bun's CI runs a static instruction scanner (scripts/verify-baseline-static/) that objdumps the release binary and flags any function containing instructions above the baseline CPU (Nehalem on x64, Cortex-A53 on aarch64). Runtime-dispatched kernels are exempted by listing their mangled symbols in allowlist-{x64,x64-windows,aarch64}.txt. Every other kernel in highway_strings.cpp — IndexOfNewlineOrNonASCIIImpl, FirstNonAscii8Impl, CopyU16ToU8Impl, etc. — has entries in all three files. ClassifyUTF8WidthImpl does not (grep of scripts/verify-baseline-static/ returns zero hits), and the PR's changed-files list doesn't include any allowlist file.

How it manifests

The robobun CI comment on this PR already reports the failure:

  • Linux aarch64: _ZN3bun10N_SVE2_12821ClassifyUTF8WidthImplEPKhm [SVE] (11 insns)
  • Linux x64: _ZN3bun10N_AVX3_SPR21ClassifyUTF8WidthImplEPKhm [AVX, AVX512BW, AVX512F] (12 insns)

with the scanner's own guidance: "If these are runtime-dispatched behind a CPUID gate: add each symbol to scripts/verify-baseline-static/allowlist-<arch>.txt with a comment pointing at the gate."

Step-by-step proof

  1. highway_strings.cpp:2218 adds HWY_EXPORT(ClassifyUTF8WidthImpl); in the HWY_ONCE block.
  2. Highway's foreach_target.h re-includes the TU once per target; each pass compiles bun::N_<TARGET>::ClassifyUTF8WidthImpl with that target's -mavx512bw / +sve flags.
  3. The scanner disassembles the release binary, finds e.g. vpcmpub / whilelo inside _ZN3bun10N_AVX3_SPR21ClassifyUTF8WidthImplEPKhm, looks up the symbol in allowlist-x64.txt, finds no match, and fails the job.
  4. Sibling kernel IndexOfNewlineOrNonASCIIImpl (nearly identical structure — one Gt compare per block) has 6 entries in allowlist-x64.txt (lines 492, 529, 566, 604, 637, 674), 4 in allowlist-aarch64.txt, and 6 in allowlist-x64-windows.txt. ClassifyUTF8WidthImpl has 0 in any of them.

Why existing code doesn't prevent it

There is no wildcard or namespace-level allowlisting — each mangled symbol must be listed individually. The scanner's message explicitly says so.

Impact

This is a hard CI failure on linux-x64, linux-x64-musl, linux-aarch64, linux-aarch64-musl, and windows-x64. The PR cannot merge until the allowlists are updated. There is no runtime correctness issue — the kernel is properly gated behind HWY_DYNAMIC_DISPATCH → hwy::SupportedTargets(), so a baseline CPU would never execute the AVX-512/SVE variants — but the static scanner cannot know that without the allowlist entry.

How to fix

Add per-target entries for ClassifyUTF8WidthImpl to all three allowlist files, mirroring the existing IndexOfNewlineOrNonASCIIImpl entries (same targets, same feature-set brackets — the kernel uses the same Gt/AllFalse/FindFirstTrue primitives). For example in allowlist-x64.txt:

_ZN3bun10N_AVX3_SPR21ClassifyUTF8WidthImplEPKhm                                [AVX, AVX512BW, AVX512F]
_ZN3bun11N_AVX3_ZEN421ClassifyUTF8WidthImplEPKhm                               [AVX, AVX512BW, AVX512F]
_ZN3bun6N_AVX221ClassifyUTF8WidthImplEPKhm                                     [AVX, AVX2]
_ZN3bun6N_AVX321ClassifyUTF8WidthImplEPKhm                                     [AVX, AVX512BW, AVX512F]
_ZN3bun9N_AVX10_221ClassifyUTF8WidthImplEPKhm                                  [AVX, AVX512BW, AVX512F]
_ZN3bun9N_AVX3_DL21ClassifyUTF8WidthImplEPKhm                                  [AVX, AVX512BW, AVX512F]

in allowlist-aarch64.txt:

_ZN3bun10N_SVE2_12821ClassifyUTF8WidthImplEPKhm                               [SVE]
_ZN3bun5N_SVE21ClassifyUTF8WidthImplEPKhm                                     [SVE]
_ZN3bun6N_SVE221ClassifyUTF8WidthImplEPKhm                                    [SVE]
_ZN3bun9N_SVE_25621ClassifyUTF8WidthImplEPKhm                                 [SVE]

and the demangled equivalents (bun::N_AVX3_SPR::ClassifyUTF8WidthImpl, etc.) in allowlist-x64-windows.txt. The exact feature-set brackets are whatever the scanner reports on the next CI run; the safest approach is to copy the brackets from IndexOfNewlineOrNonASCIIImpl (same primitives) and let CI confirm.

{
D8 d;
const size_t N = hn::Lanes(d);

const auto vec_max_ascii = hn::Set(d, uint8_t { 0x7F });
const auto vec_max_latin1_lead = hn::Set(d, uint8_t { 0xC3 });

const size_t simd_text_len = search_len - (search_len % N);
size_t i = 0;

for (; i < simd_text_len; i += N) {
const auto vec = hn::LoadU(d, start_ptr + i);
const auto mask_non_ascii = hn::Gt(vec, vec_max_ascii);
if (hn::AllFalse(d, mask_non_ascii)) {
continue;
}
const size_t packed_first = (i + static_cast<size_t>(hn::FindFirstTrue(d, mask_non_ascii))) << 2;
for (; i < simd_text_len; i += N) {
const auto v = hn::LoadU(d, start_ptr + i);
if (!hn::AllFalse(d, hn::Gt(v, vec_max_latin1_lead))) {
return packed_first | 2;
}
}
for (; i < search_len; ++i) {
if (start_ptr[i] >= 0xC4) {
return packed_first | 2;
}
}
return packed_first | 1;
}

for (; i < search_len; ++i) {
const uint8_t c = start_ptr[i];
if (c < 0x80) {
continue;
}
const size_t packed_first = i << 2;
for (; i < search_len; ++i) {
if (start_ptr[i] >= 0xC4) {
return packed_first | 2;
}
}
return packed_first | 1;
}

return search_len << 2;
}

size_t IndexOfNewlineOrNonASCIIImpl(const uint8_t* HWY_RESTRICT start_ptr, size_t search_len)
{
ASSERT(search_len > 0);
Expand Down Expand Up @@ -2158,6 +2215,7 @@
HWY_EXPORT(IndexOfInterestingCharacterInStringLiteralImpl);
HWY_EXPORT(IndexOfNeedsEscapeForJavaScriptStringImplBacktick);
HWY_EXPORT(IndexOfNeedsEscapeForJavaScriptStringImplQuote);
HWY_EXPORT(ClassifyUTF8WidthImpl);
HWY_EXPORT(IndexOfNewlineOrNonASCIIImpl);
HWY_EXPORT(IndexOfNewlineOrNonASCIIOrHashOrAtImpl);
HWY_EXPORT(IndexOfSpaceOrNewlineOrNonASCIIImpl);
Expand Down Expand Up @@ -2310,6 +2368,11 @@
return HWY_DYNAMIC_DISPATCH(IndexOfNewlineOrNonASCIIImpl)(haystack, haystack_len);
}

size_t highway_classify_utf8_width(const uint8_t* HWY_RESTRICT bytes, size_t len)
{
return HWY_DYNAMIC_DISPATCH(ClassifyUTF8WidthImpl)(bytes, len);
}

size_t highway_index_of_newline_or_non_ascii_or_hash_or_at(const uint8_t* HWY_RESTRICT haystack, size_t haystack_len)
{
return HWY_DYNAMIC_DISPATCH(IndexOfNewlineOrNonASCIIOrHashOrAtImpl)(haystack, haystack_len);
Expand Down
Loading
Loading