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
16 changes: 16 additions & 0 deletions regex-syntax/benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,19 @@ fn parse_huge(b: &mut Bencher) {
Parser::new().parse(re).unwrap()
});
}

#[bench]
fn parse_full_folding(b: &mut Bencher) {
b.iter(|| {
let re = r"(?i)[_a-z\x7F-\x{10FFFF}]";
Parser::new().parse(re).unwrap()
});
}

#[bench]
fn parse_simple_folding(b: &mut Bencher) {
b.iter(|| {
let re = r"(?i)abc";
Parser::new().parse(re).unwrap()
});
}
10 changes: 3 additions & 7 deletions regex-syntax/src/hir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1291,13 +1291,9 @@ impl Interval for ClassUnicodeRange {
&self,
ranges: &mut Vec<ClassUnicodeRange>,
) -> Result<(), unicode::CaseFoldError> {
let mut folder = unicode::SimpleCaseFolder::new()?;
if !folder.overlaps(self.start, self.end) {
return Ok(());
}
let (start, end) = (u32::from(self.start), u32::from(self.end));
for cp in (start..=end).filter_map(char::from_u32) {
for &cp_folded in folder.mapping(cp) {
let folder = unicode::SimpleCaseFolder::new()?;
for folded in folder.mapping_in_range(self.start, self.end) {
for &cp_folded in folded {
ranges.push(ClassUnicodeRange::new(cp_folded, cp_folded));
}
}
Expand Down
122 changes: 27 additions & 95 deletions regex-syntax/src/unicode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,20 @@ impl core::fmt::Display for UnicodeWordError {
}
}

/// A state oriented traverser of the simple case folding table.
/// A simple case folding table.
///
/// A case folder can be constructed via `SimpleCaseFolder::new()`, which will
/// return an error if the underlying case folding table is unavailable.
///
/// After construction, it is expected that callers will use
/// `SimpleCaseFolder::mapping` by calling it with codepoints in strictly
/// increasing order. For example, calling it on `b` and then on `a` is illegal
/// and will result in a panic.
///
/// The main idea of this type is that it tries hard to make mapping lookups
/// fast by exploiting the structure of the underlying table, and the ordering
/// assumption enables this.
/// Since the table is sorted, [`SimpleCaseFolder::mapping_in_range`] finds the
/// first entry and then returns only entries contained in the range.
#[derive(Debug)]
pub struct SimpleCaseFolder {
/// The simple case fold table. It's a sorted association list, where the
/// keys are Unicode scalar values and the values are the corresponding
/// equivalence class (not including the key) of the "simple" case folded
/// Unicode scalar values.
table: &'static [(char, &'static [char])],
/// The last codepoint that was used for a lookup.
last: Option<char>,
/// The index to the entry in `table` corresponding to the smallest key `k`
/// such that `k > k0`, where `k0` is the most recent key lookup. Note that
/// in particular, `k0` may not be in the table!
next: usize,
}

impl SimpleCaseFolder {
Expand All @@ -104,99 +92,40 @@ impl SimpleCaseFolder {
{
Ok(SimpleCaseFolder {
table: crate::unicode_tables::case_folding_simple::CASE_FOLDING_SIMPLE,
last: None,
next: 0,
})
}
}

/// Return the equivalence class of case folded codepoints for the given
/// codepoint. The equivalence class returned never includes the codepoint
/// given. If the given codepoint has no case folded codepoints (i.e.,
/// no entry in the underlying case folding table), then this returns an
/// empty slice.
/// Return an iterator to every entry in the simple fold table where the
/// key is in the inclusive range `[start, end]`.
///
/// # Panics
///
/// This panics when called with a `c` that is less than or equal to the
/// previous call. In other words, callers need to use this method with
/// strictly increasing values of `c`.
pub fn mapping(&mut self, c: char) -> &'static [char] {
if let Some(last) = self.last {
assert!(
last < c,
"got codepoint U+{:X} which occurs before \
last codepoint U+{:X}",
u32::from(c),
u32::from(last),
);
}
self.last = Some(c);
if self.next >= self.table.len() {
return &[];
}
let (k, v) = self.table[self.next];
if k == c {
self.next += 1;
return v;
}
match self.get(c) {
Err(i) => {
self.next = i;
&[]
}
Ok(i) => {
// Since we require lookups to proceed
// in order, anything we find should be
// after whatever we thought might be
// next. Otherwise, the caller is either
// going out of order or we would have
// found our next key at 'self.next'.
assert!(i > self.next);
self.next = i + 1;
self.table[i].1
}
}
/// This panics when `end < start`.
pub fn mapping_in_range(
&self,
start: char,
end: char,
) -> impl Iterator<Item = &'static [char]> {
assert!(
start <= end,
"tried to get a mapping but end is before start"
);
let i = self.table.partition_point(|&(c, _)| c < start);
self.table[i..]
.iter()
.take_while(move |&&(c, _)| c <= end)
.map(|&(_, folded)| folded)
}

/// Returns true if and only if the given range overlaps with any region
/// of the underlying case folding table. That is, when true, there exists
/// at least one codepoint in the inclusive range `[start, end]` that has
/// a non-trivial equivalence class of case folded codepoints. Conversely,
/// when this returns false, all codepoints in the range `[start, end]`
/// correspond to the trivial equivalence class of case folded codepoints,
/// i.e., itself.
///
/// This is useful to call before iterating over the codepoints in the
/// range and looking up the mapping for each. If you know none of the
/// mappings will return anything, then you might be able to skip doing it
/// altogether.
/// Returns true if and only if at least one codepoint in the
/// inclusive range `[start, end]` has a non-trivial simple case folding.
///
/// # Panics
///
/// This panics when `end < start`.
pub fn overlaps(&self, start: char, end: char) -> bool {
use core::cmp::Ordering;

assert!(start <= end);
self.table
.binary_search_by(|&(c, _)| {
if start <= c && c <= end {
Ordering::Equal
} else if c > end {
Ordering::Greater
} else {
Ordering::Less
}
})
.is_ok()
}

/// Returns the index at which `c` occurs in the simple case fold table. If
/// `c` does not occur, then this returns an `i` such that `table[i-1].0 <
/// c` and `table[i].0 > c`.
fn get(&self, c: char) -> Result<usize, usize> {
self.table.binary_search_by_key(&c, |&(c1, _)| c1)
self.mapping_in_range(start, end).next().is_some()
}
}

Expand Down Expand Up @@ -950,7 +879,10 @@ mod tests {

#[cfg(feature = "unicode-case")]
fn simple_fold_ok(c: char) -> impl Iterator<Item = char> {
SimpleCaseFolder::new().unwrap().mapping(c).iter().copied()
SimpleCaseFolder::new()
.unwrap()
.mapping_in_range(c, c)
.flat_map(|folded| folded.iter().copied())
}

#[cfg(feature = "unicode-case")]
Expand Down