Repository navigation
encoding: materialize 8-bit strings from UTF-8 decode when every code point fits in Latin-1 #36615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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); | ||
|
|
@@ -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`. | ||
|
|
@@ -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]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() } }; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The new Highway kernel Extended reasoning...What the bug is
Bun's CI runs a static instruction scanner ( How it manifestsThe robobun CI comment on this PR already reports the failure:
with the scanner's own guidance: "If these are runtime-dispatched behind a CPUID gate: add each symbol to Step-by-step proof
Why existing code doesn't prevent itThere is no wildcard or namespace-level allowlisting — each mangled symbol must be listed individually. The scanner's message explicitly says so. ImpactThis is a hard CI failure on How to fixAdd per-target entries for in and the demangled equivalents ( |
||
| { | ||
| 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); | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
||
There was a problem hiding this comment.
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