From 4b585c72b5edbeb0dafb11afe2fd702ef933d3a4 Mon Sep 17 00:00:00 2001 From: SomeFlyingThing <306498559+SomeFlyingThing@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:14:20 -0700 Subject: [PATCH 1/2] Hint that str search results are in bounds --- library/core/src/str/mod.rs | 14 ++++++-- .../str-find-result-bounds.rs | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/codegen-llvm/lib-optimizations/str-find-result-bounds.rs diff --git a/library/core/src/str/mod.rs b/library/core/src/str/mod.rs index 06f3edccc25b2..4a29124cffea1 100644 --- a/library/core/src/str/mod.rs +++ b/library/core/src/str/mod.rs @@ -1494,7 +1494,12 @@ impl str { #[stable(feature = "rust1", since = "1.0.0")] #[inline] pub fn find(&self, pat: P) -> Option { - pat.into_searcher(self).next_match().map(|(i, _)| i) + let result = pat.into_searcher(self).next_match().map(|(i, _)| i); + if let Some(index) = result { + // SAFETY: `Searcher` implementations must return ranges within the haystack. + unsafe { assert_unchecked(index <= self.len()) }; + } + result } /// Returns the byte index for the first character of the last match of the pattern in @@ -1543,7 +1548,12 @@ impl str { where for<'a> P::Searcher<'a>: ReverseSearcher<'a>, { - pat.into_searcher(self).next_match_back().map(|(i, _)| i) + let result = pat.into_searcher(self).next_match_back().map(|(i, _)| i); + if let Some(index) = result { + // SAFETY: `Searcher` implementations must return ranges within the haystack. + unsafe { assert_unchecked(index <= self.len()) }; + } + result } /// Returns an iterator over substrings of this string slice, separated by diff --git a/tests/codegen-llvm/lib-optimizations/str-find-result-bounds.rs b/tests/codegen-llvm/lib-optimizations/str-find-result-bounds.rs new file mode 100644 index 0000000000000..932245c14840d --- /dev/null +++ b/tests/codegen-llvm/lib-optimizations/str-find-result-bounds.rs @@ -0,0 +1,36 @@ +//@ compile-flags: -Copt-level=3 + +#![crate_type = "lib"] + +// Make sure no bounds checks are emitted when slicing with an index returned +// by `str::find` or `str::rfind`. + +// CHECK-LABEL: @find_str_prefix_no_bounds_check +#[no_mangle] +pub fn find_str_prefix_no_bounds_check<'a>(haystack: &'a str, needle: &str) -> &'a [u8] { + // CHECK-NOT: slice_index_fail + match haystack.find(needle) { + Some(index) => &haystack.as_bytes()[..index], + None => haystack.as_bytes(), + } +} + +// CHECK-LABEL: @rfind_char_suffix_no_bounds_check +#[no_mangle] +pub fn rfind_char_suffix_no_bounds_check(haystack: &str, needle: char) -> &[u8] { + // CHECK-NOT: slice_index_fail + match haystack.rfind(needle) { + Some(index) => &haystack.as_bytes()[index..], + None => haystack.as_bytes(), + } +} + +// CHECK-LABEL: @rfind_str_suffix_no_bounds_check +#[no_mangle] +pub fn rfind_str_suffix_no_bounds_check<'a>(haystack: &'a str, needle: &str) -> &'a [u8] { + // CHECK-NOT: slice_index_fail + match haystack.rfind(needle) { + Some(index) => &haystack.as_bytes()[index..], + None => haystack.as_bytes(), + } +} From 865919862fa24a2c7218cdbf281e55311a652275 Mon Sep 17 00:00:00 2001 From: SomeFlyingThing <306498559+SomeFlyingThing@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:09:05 -0700 Subject: [PATCH 2/2] Hint that character search results are in bounds --- library/core/src/str/pattern.rs | 16 ++++++- .../str-find-index-no-bound-check.rs | 43 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/codegen-llvm/str-find-index-no-bound-check.rs diff --git a/library/core/src/str/pattern.rs b/library/core/src/str/pattern.rs index c014bc2818b0e..81d15ee53129c 100644 --- a/library/core/src/str/pattern.rs +++ b/library/core/src/str/pattern.rs @@ -461,6 +461,10 @@ unsafe impl<'a> Searcher<'a> for CharSearcher<'a> { let found_char = self.finger - self.utf8_size(); if let Some(slice) = self.haystack.as_bytes().get(found_char..self.finger) { if slice == &self.utf8_encoded[0..self.utf8_size()] { + // SAFETY: `slice` is a nonempty UTF-8 encoding found in the haystack. + unsafe { + crate::hint::assert_unchecked(found_char < self.haystack.len()) + }; return Some((found_char, self.finger)); } } @@ -521,6 +525,8 @@ unsafe impl<'a> ReverseSearcher<'a> for CharSearcher<'a> { let found_char = index - shift; if let Some(slice) = haystack.get(found_char..(found_char + self.utf8_size())) { if slice == &self.utf8_encoded[0..self.utf8_size()] { + // SAFETY: `slice` is a nonempty UTF-8 encoding found in the haystack. + unsafe { crate::hint::assert_unchecked(found_char < haystack.len()) }; // move finger to before the character found (i.e., at its start index) self.finger_back = found_char; return Some((self.finger_back, self.finger_back + self.utf8_size())); @@ -784,7 +790,10 @@ macro_rules! searcher_methods { } #[inline] fn next_match(&mut self) -> Option<(usize, usize)> { - self.0.next_match() + let (start, end) = self.0.next_match()?; + // SAFETY: these searchers only match nonempty chars in the haystack. + unsafe { crate::hint::assert_unchecked(start < self.0.haystack.len()) }; + Some((start, end)) } #[inline] fn next_reject(&mut self) -> Option<(usize, usize)> { @@ -798,7 +807,10 @@ macro_rules! searcher_methods { } #[inline] fn next_match_back(&mut self) -> Option<(usize, usize)> { - self.0.next_match_back() + let (start, end) = self.0.next_match_back()?; + // SAFETY: these searchers only match nonempty chars in the haystack. + unsafe { crate::hint::assert_unchecked(start < self.0.haystack.len()) }; + Some((start, end)) } #[inline] fn next_reject_back(&mut self) -> Option<(usize, usize)> { diff --git a/tests/codegen-llvm/str-find-index-no-bound-check.rs b/tests/codegen-llvm/str-find-index-no-bound-check.rs new file mode 100644 index 0000000000000..052cb738270b1 --- /dev/null +++ b/tests/codegen-llvm/str-find-index-no-bound-check.rs @@ -0,0 +1,43 @@ +//@ compile-flags: -Copt-level=3 -C panic=abort +#![crate_type = "lib"] +#![no_std] + +// A successful search for a `char` always returns the start of a nonempty +// match, so its byte index is valid for the original string. + +// Verify that the check would be visible in the generated IR. + +// CHECK-LABEL: @bounds_check_is_visible +#[no_mangle] +pub fn bounds_check_is_visible(s: &str, index: usize) -> u8 { + // CHECK: call{{.*}}panic_bounds_check + s.as_bytes()[index] +} + +// CHECK-LABEL: @find_char_index_no_bounds_check +#[no_mangle] +pub fn find_char_index_no_bounds_check(s: &str, needle: char) -> u8 { + // CHECK-NOT: call{{.*}}panic_bounds_check + if let Some(index) = s.find(needle) { s.as_bytes()[index] } else { 0 } +} + +// CHECK-LABEL: @find_predicate_index_no_bounds_check +#[no_mangle] +pub fn find_predicate_index_no_bounds_check(s: &str, needle: char) -> u8 { + // CHECK-NOT: call{{.*}}panic_bounds_check + if let Some(index) = s.find(|c| c == needle) { s.as_bytes()[index] } else { 0 } +} + +// CHECK-LABEL: @rfind_char_index_no_bounds_check +#[no_mangle] +pub fn rfind_char_index_no_bounds_check(s: &str, needle: char) -> u8 { + // CHECK-NOT: call{{.*}}panic_bounds_check + if let Some(index) = s.rfind(needle) { s.as_bytes()[index] } else { 0 } +} + +// CHECK-LABEL: @rfind_predicate_index_no_bounds_check +#[no_mangle] +pub fn rfind_predicate_index_no_bounds_check(s: &str, needle: char) -> u8 { + // CHECK-NOT: call{{.*}}panic_bounds_check + if let Some(index) = s.rfind(|c| c == needle) { s.as_bytes()[index] } else { 0 } +}