From 8287aec51decb9d50404a97ea5f009523be88e28 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Sun, 5 Jul 2026 13:20:41 +0100 Subject: [PATCH] refactor(c): dedup FFI guard boilerplate and SIMD scalar tails The repeated handle/lock/init matches in fff-c collapse into try_inst!/try_cstr!/read_picker! macros, and the free/accessor bodies share free_array/elem_at helpers. The NEON and AVX2 memmem kernels now share one packed_pair_tail and verify_scalar_from. Also drops the stale fff-core copy of parse_bench.rs left behind by the query-parser crate extraction. Exported C symbols and header unchanged. --- crates/fff-c/src/lib.rs | 415 ++++++------------ crates/fff-core/Cargo.toml | 4 - crates/fff-core/benches/parse_bench.rs | 180 -------- .../fff-core/src/case_insensitive_memmem.rs | 173 +++----- 4 files changed, 201 insertions(+), 571 deletions(-) delete mode 100644 crates/fff-core/benches/parse_bench.rs diff --git a/crates/fff-c/src/lib.rs b/crates/fff-c/src/lib.rs index 56283d7e..c515676a 100644 --- a/crates/fff-c/src/lib.rs +++ b/crates/fff-c/src/lib.rs @@ -105,6 +105,74 @@ fn default_i32(val: i32, default: i32) -> i32 { if val == 0 { default } else { val } } +/// Recover the instance from the opaque handle or early-return the error result. +macro_rules! try_inst { + ($handle:expr) => { + match unsafe { instance_ref($handle) } { + Ok(i) => i, + Err(e) => return e, + } + }; +} + +/// Convert a required C string or early-return an error result. +macro_rules! try_cstr { + ($ptr:expr, $msg:expr) => { + match unsafe { cstr_to_str($ptr) } { + Some(s) => s, + None => return FffResult::err($msg), + } + }; +} + +/// Bind `$guard`/`$picker` to a read-locked picker ref, early-returning on +/// poisoned lock or uninitialized picker. +macro_rules! read_picker { + ($inst:expr, $guard:ident, $picker:ident) => { + let $guard = match $inst.picker.read() { + Ok(g) => g, + Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), + }; + let $picker = match $guard.as_ref() { + Some(p) => p, + None => { + return FffResult::err( + "File picker not initialized. Call fff_create_instance first.", + ); + } + }; + }; +} + +/// Base path of the current picker, or `None` if the lock is poisoned or the +/// picker is not initialized. +fn project_path(inst: &FffInstance) -> Option { + inst.picker + .read() + .ok()? + .as_ref() + .map(|p| p.base_path().to_path_buf()) +} + +/// Reconstruct a heap array and run `f` on each element before dropping it. +unsafe fn free_array(ptr: *mut T, count: usize, f: impl Fn(&mut T)) { + if !ptr.is_null() { + let mut items = unsafe { Vec::from_raw_parts(ptr, count, count) }; + for item in &mut items { + f(item); + } + } +} + +/// Pointer to the `index`-th element of a result array, or null when out of +/// bounds. Shared body of the `fff_*_get_*` accessors. +unsafe fn elem_at(items: *const T, count: u32, index: u32) -> *const T { + if items.is_null() || index >= count { + return std::ptr::null(); + } + unsafe { items.add(index as usize) } +} + /// Create a new file finder instance (legacy 8-arg positional signature). /// /// @deprecated Use [`fff_create_instance_with`] (or @@ -382,32 +450,16 @@ pub unsafe extern "C" fn fff_search( combo_boost_multiplier: i32, min_combo_count: u32, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let query_str = match unsafe { cstr_to_str(query) } { - Some(s) => s, - None => return FffResult::err("Query is null or invalid UTF-8"), - }; + let query_str = try_cstr!(query, "Query is null or invalid UTF-8"); let current_file_str = unsafe { optional_cstr(current_file) }; let page_size = default_u32(page_size, 100) as usize; let min_combo_count = default_u32(min_combo_count, 3); let combo_boost_multiplier = default_i32(combo_boost_multiplier, 100); - let picker_guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match picker_guard.as_ref() { - Some(p) => p, - None => { - return FffResult::err("File picker not initialized. Call fff_create_instance first."); - } - }; + read_picker!(inst, picker_guard, picker); // Get query tracker ref for combo matching let qt_guard = match inst.query_tracker.read() { @@ -468,10 +520,7 @@ pub unsafe extern "C" fn fff_glob( page_index: u32, page_size: u32, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); let pattern_str = match unsafe { cstr_to_str(pattern) } { Some(s) if !s.is_empty() => s, @@ -481,17 +530,7 @@ pub unsafe extern "C" fn fff_glob( let current_file_str = unsafe { optional_cstr(current_file) }; let page_size = default_u32(page_size, 100) as usize; - let picker_guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match picker_guard.as_ref() { - Some(p) => p, - None => { - return FffResult::err("File picker not initialized. Call fff_create_instance first."); - } - }; + read_picker!(inst, picker_guard, picker); let results = picker.glob( pattern_str, @@ -535,30 +574,14 @@ pub unsafe extern "C" fn fff_search_directories( page_index: u32, page_size: u32, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let query_str = match unsafe { cstr_to_str(query) } { - Some(s) => s, - None => return FffResult::err("Query is null or invalid UTF-8"), - }; + let query_str = try_cstr!(query, "Query is null or invalid UTF-8"); let current_file_str = unsafe { optional_cstr(current_file) }; let page_size = default_u32(page_size, 100) as usize; - let picker_guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match picker_guard.as_ref() { - Some(p) => p, - None => { - return FffResult::err("File picker not initialized. Call fff_create_instance first."); - } - }; + read_picker!(inst, picker_guard, picker); let parser = QueryParser::new(fff_query_parser::DirSearchConfig); let parsed = parser.parse(query_str); @@ -613,32 +636,16 @@ pub unsafe extern "C" fn fff_search_mixed( combo_boost_multiplier: i32, min_combo_count: u32, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let query_str = match unsafe { cstr_to_str(query) } { - Some(s) => s, - None => return FffResult::err("Query is null or invalid UTF-8"), - }; + let query_str = try_cstr!(query, "Query is null or invalid UTF-8"); let current_file_str = unsafe { optional_cstr(current_file) }; let page_size = default_u32(page_size, 100) as usize; let min_combo_count = default_u32(min_combo_count, 3); let combo_boost_multiplier = default_i32(combo_boost_multiplier, 100); - let picker_guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match picker_guard.as_ref() { - Some(p) => p, - None => { - return FffResult::err("File picker not initialized. Call fff_create_instance first."); - } - }; + read_picker!(inst, picker_guard, picker); let qt_guard = match inst.query_tracker.read() { Ok(q) => q, @@ -704,27 +711,11 @@ pub unsafe extern "C" fn fff_live_grep( after_context: u32, classify_definitions: bool, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; - - let query_str = match unsafe { cstr_to_str(query) } { - Some(s) => s, - None => return FffResult::err("Query is null or invalid UTF-8"), - }; + let inst = try_inst!(fff_handle); - let picker_guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; + let query_str = try_cstr!(query, "Query is null or invalid UTF-8"); - let picker = match picker_guard.as_ref() { - Some(p) => p, - None => { - return FffResult::err("File picker not initialized. Call fff_create_instance first."); - } - }; + read_picker!(inst, picker_guard, picker); let is_ai = picker.mode().is_ai(); let parsed = if is_ai { @@ -791,10 +782,7 @@ pub unsafe extern "C" fn fff_multi_grep( after_context: u32, classify_definitions: bool, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); let patterns_str = match unsafe { cstr_to_str(patterns_joined) } { Some(s) if !s.is_empty() => s, @@ -808,17 +796,7 @@ pub unsafe extern "C" fn fff_multi_grep( let constraints_str = unsafe { optional_cstr(constraints) }; - let picker_guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match picker_guard.as_ref() { - Some(p) => p, - None => { - return FffResult::err("File picker not initialized. Call fff_create_instance first."); - } - }; + read_picker!(inst, picker_guard, picker); let is_ai = picker.mode().is_ai(); @@ -862,10 +840,7 @@ pub unsafe extern "C" fn fff_multi_grep( /// `fff_handle` must be a valid instance pointer from `fff_create_instance`. #[unsafe(no_mangle)] pub unsafe extern "C" fn fff_scan_files(fff_handle: *mut c_void) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); // Async: rescan runs on a BG thread, caller returns immediately. // Use `fff_is_scanning` / `fff_wait_for_scan` to observe progress. @@ -902,20 +877,9 @@ pub unsafe extern "C" fn fff_is_scanning(fff_handle: *mut c_void) -> bool { /// `fff_handle` must be a valid instance pointer from `fff_create_instance`. #[unsafe(no_mangle)] pub unsafe extern "C" fn fff_get_base_path(fff_handle: *mut c_void) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match guard.as_ref() { - Some(p) => p, - None => return FffResult::err("File picker not initialized"), - }; + read_picker!(inst, guard, picker); FffResult::ok_string(&picker.base_path().to_string_lossy()) } @@ -926,20 +890,9 @@ pub unsafe extern "C" fn fff_get_base_path(fff_handle: *mut c_void) -> *mut FffR /// `fff_handle` must be a valid instance pointer from `fff_create_instance`. #[unsafe(no_mangle)] pub unsafe extern "C" fn fff_get_scan_progress(fff_handle: *mut c_void) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let guard = match inst.picker.read() { - Ok(g) => g, - Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), - }; - - let picker = match guard.as_ref() { - Some(p) => p, - None => return FffResult::err("File picker not initialized"), - }; + read_picker!(inst, guard, picker); let result = Box::into_raw(Box::new(FffScanProgress::from(picker.get_scan_progress()))); FffResult::ok_handle(result as *mut c_void) @@ -954,10 +907,7 @@ pub unsafe extern "C" fn fff_wait_for_scan( fff_handle: *mut c_void, timeout_ms: u64, ) -> *mut FffResult { - let FffInstance { picker, .. } = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let FffInstance { picker, .. } = try_inst!(fff_handle); let completed = picker.wait_for_scan(Duration::from_millis(timeout_ms)); FffResult::ok_int(completed as i64) @@ -972,10 +922,7 @@ pub unsafe extern "C" fn fff_wait_for_watcher( fff_handle: *mut c_void, timeout_ms: u64, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); let completed = inst .picker @@ -993,15 +940,9 @@ pub unsafe extern "C" fn fff_restart_index( fff_handle: *mut c_void, new_path: *const c_char, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let path_str = match unsafe { cstr_to_str(new_path) } { - Some(s) => s, - None => return FffResult::err("Path is null or invalid UTF-8"), - }; + let path_str = try_cstr!(new_path, "Path is null or invalid UTF-8"); let path = PathBuf::from(&path_str); if !path.exists() { @@ -1061,10 +1002,7 @@ pub unsafe extern "C" fn fff_restart_index( /// `fff_handle` must be a valid instance pointer from `fff_create_instance`. #[unsafe(no_mangle)] pub unsafe extern "C" fn fff_refresh_git_status(fff_handle: *mut c_void) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); match inst.picker.refresh_git_status(&inst.frecency) { Ok(count) => FffResult::ok_int(count as i64), @@ -1083,35 +1021,19 @@ pub unsafe extern "C" fn fff_track_query( query: *const c_char, file_path: *const c_char, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let query_str = match unsafe { cstr_to_str(query) } { - Some(s) => s, - None => return FffResult::err("Query is null or invalid UTF-8"), - }; + let query_str = try_cstr!(query, "Query is null or invalid UTF-8"); - let path_str = match unsafe { cstr_to_str(file_path) } { - Some(s) => s, - None => return FffResult::err("File path is null or invalid UTF-8"), - }; + let path_str = try_cstr!(file_path, "File path is null or invalid UTF-8"); let file_path = match fff::path_utils::canonicalize(path_str) { Ok(p) => p, Err(e) => return FffResult::err(&format!("Failed to canonicalize path: {}", e)), }; - let project_path = { - let guard = match inst.picker.read() { - Ok(g) => g, - Err(_) => return FffResult::ok_int(0), - }; - match guard.as_ref() { - Some(p) => p.base_path().to_path_buf(), - None => return FffResult::ok_int(0), - } + let Some(project_path) = project_path(inst) else { + return FffResult::ok_int(0); }; let mut qt_guard = match inst.query_tracker.write() { @@ -1137,20 +1059,10 @@ pub unsafe extern "C" fn fff_get_historical_query( fff_handle: *mut c_void, offset: u64, ) -> *mut FffResult { - let inst = match unsafe { instance_ref(fff_handle) } { - Ok(i) => i, - Err(e) => return e, - }; + let inst = try_inst!(fff_handle); - let project_path = { - let guard = match inst.picker.read() { - Ok(g) => g, - Err(_) => return FffResult::ok_empty(), - }; - match guard.as_ref() { - Some(p) => p.base_path().to_path_buf(), - None => return FffResult::ok_empty(), - } + let Some(project_path) = project_path(inst) else { + return FffResult::ok_empty(); }; let qt_guard = match inst.query_tracker.read() { @@ -1375,19 +1287,8 @@ pub unsafe extern "C" fn fff_free_search_result(result: *mut FffSearchResult) { unsafe { let result = Box::from_raw(result); let count = result.count as usize; - - if !result.items.is_null() { - let mut items = Vec::from_raw_parts(result.items, count, count); - for item in &mut items { - item.free_strings(); - } - } - if !result.scores.is_null() { - let mut scores = Vec::from_raw_parts(result.scores, count, count); - for score in &mut scores { - score.free_strings(); - } - } + free_array(result.items, count, |item| item.free_strings()); + free_array(result.scores, count, |score| score.free_strings()); } } @@ -1403,14 +1304,10 @@ pub unsafe extern "C" fn fff_search_result_get_item( result: *const FffSearchResult, index: u32, ) -> *const FffFileItem { - if result.is_null() { - return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.items.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - unsafe { result.items.add(index as usize) } + }; + unsafe { elem_at(result.items, result.count, index) } } /// Get a pointer to the `index`-th `FffScore` in a search result. @@ -1425,14 +1322,10 @@ pub unsafe extern "C" fn fff_search_result_get_score( result: *const FffSearchResult, index: u32, ) -> *const FffScore { - if result.is_null() { - return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.scores.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - unsafe { result.scores.add(index as usize) } + }; + unsafe { elem_at(result.scores, result.count, index) } } /// Free a grep result returned by `fff_live_grep` or `fff_multi_grep`. @@ -1452,13 +1345,7 @@ pub unsafe extern "C" fn fff_free_grep_result(result: *mut FffGrepResult) { unsafe { let result = Box::from_raw(result); let count = result.count as usize; - - if !result.items.is_null() { - let mut items = Vec::from_raw_parts(result.items, count, count); - for item in &mut items { - item.free_fields(); - } - } + free_array(result.items, count, |item| item.free_fields()); if !result.regex_fallback_error.is_null() { drop(CString::from_raw(result.regex_fallback_error)); } @@ -1477,14 +1364,10 @@ pub unsafe extern "C" fn fff_grep_result_get_match( result: *const FffGrepResult, index: u32, ) -> *const FffGrepMatch { - if result.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.items.is_null() { - return std::ptr::null(); - } - unsafe { result.items.add(index as usize) } + }; + unsafe { elem_at(result.items, result.count, index) } } /// Free a scan progress result returned by `fff_get_scan_progress`. @@ -1572,19 +1455,8 @@ pub unsafe extern "C" fn fff_free_dir_search_result(result: *mut FffDirSearchRes unsafe { let result = Box::from_raw(result); let count = result.count as usize; - - if !result.items.is_null() { - let mut items = Vec::from_raw_parts(result.items, count, count); - for item in &mut items { - item.free_strings(); - } - } - if !result.scores.is_null() { - let mut scores = Vec::from_raw_parts(result.scores, count, count); - for score in &mut scores { - score.free_strings(); - } - } + free_array(result.items, count, |item| item.free_strings()); + free_array(result.scores, count, |score| score.free_strings()); } } @@ -1597,14 +1469,10 @@ pub unsafe extern "C" fn fff_dir_search_result_get_item( result: *const FffDirSearchResult, index: u32, ) -> *const FffDirItem { - if result.is_null() { - return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.items.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - unsafe { result.items.add(index as usize) } + }; + unsafe { elem_at(result.items, result.count, index) } } /// Get a pointer to the `index`-th `FffScore` in a directory search result. @@ -1616,14 +1484,10 @@ pub unsafe extern "C" fn fff_dir_search_result_get_score( result: *const FffDirSearchResult, index: u32, ) -> *const FffScore { - if result.is_null() { - return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.scores.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - unsafe { result.scores.add(index as usize) } + }; + unsafe { elem_at(result.scores, result.count, index) } } // --------------------------------------------------------------------------- @@ -1644,19 +1508,8 @@ pub unsafe extern "C" fn fff_free_mixed_search_result(result: *mut FffMixedSearc unsafe { let result = Box::from_raw(result); let count = result.count as usize; - - if !result.items.is_null() { - let mut items = Vec::from_raw_parts(result.items, count, count); - for item in &mut items { - item.free_strings(); - } - } - if !result.scores.is_null() { - let mut scores = Vec::from_raw_parts(result.scores, count, count); - for score in &mut scores { - score.free_strings(); - } - } + free_array(result.items, count, |item| item.free_strings()); + free_array(result.scores, count, |score| score.free_strings()); } } @@ -1669,14 +1522,10 @@ pub unsafe extern "C" fn fff_mixed_search_result_get_item( result: *const FffMixedSearchResult, index: u32, ) -> *const FffMixedItem { - if result.is_null() { - return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.items.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - unsafe { result.items.add(index as usize) } + }; + unsafe { elem_at(result.items, result.count, index) } } /// Get a pointer to the `index`-th `FffScore` in a mixed search result. @@ -1688,12 +1537,8 @@ pub unsafe extern "C" fn fff_mixed_search_result_get_score( result: *const FffMixedSearchResult, index: u32, ) -> *const FffScore { - if result.is_null() { - return std::ptr::null(); - } - let result = unsafe { &*result }; - if index >= result.count || result.scores.is_null() { + let Some(result) = (unsafe { result.as_ref() }) else { return std::ptr::null(); - } - unsafe { result.scores.add(index as usize) } + }; + unsafe { elem_at(result.scores, result.count, index) } } diff --git a/crates/fff-core/Cargo.toml b/crates/fff-core/Cargo.toml index 6ddd6c62..ed049bbf 100644 --- a/crates/fff-core/Cargo.toml +++ b/crates/fff-core/Cargo.toml @@ -10,10 +10,6 @@ description = "Faboulous & Fast File Finder - a fast and extremely correct file path = "src/lib.rs" crate-type = ["rlib", "staticlib", "cdylib"] -[[bench]] -name = "parse_bench" -harness = false - [[bench]] name = "bigram_bench" harness = false diff --git a/crates/fff-core/benches/parse_bench.rs b/crates/fff-core/benches/parse_bench.rs deleted file mode 100644 index cd6e8d28..00000000 --- a/crates/fff-core/benches/parse_bench.rs +++ /dev/null @@ -1,180 +0,0 @@ -use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; -use fff_query_parser::*; - -fn bench_parse_simple(c: &mut Criterion) { - let parser = QueryParser::default(); - - c.bench_function("parse_simple_text", |b| { - b.iter(|| parser.parse(black_box("hello world"))); - }); - - c.bench_function("parse_extension", |b| { - b.iter(|| parser.parse(black_box("*.rs"))); - }); - - c.bench_function("parse_text_with_extension", |b| { - b.iter(|| parser.parse(black_box("name *.rs"))); - }); -} - -fn bench_parse_complex(c: &mut Criterion) { - let parser = QueryParser::default(); - - c.bench_function("parse_complex_mixed", |b| { - b.iter(|| parser.parse(black_box("src name *.rs !test /lib/ status:modified"))); - }); - - c.bench_function("parse_glob", |b| { - b.iter(|| parser.parse(black_box("**/*.rs"))); - }); - - c.bench_function("parse_multiple_constraints", |b| { - b.iter(|| parser.parse(black_box("*.rs *.toml *.md !test !node_modules /src/"))); - }); -} - -fn bench_parse_realistic_queries(c: &mut Criterion) { - let parser = QueryParser::default(); - - let queries = vec![ - "file", - "test", - "mod.rs", - "src/*.rs", - "lib test", - "*.rs !test", - "src/lib/*.rs", - "/src/ name", - "status:modified *.rs", - "type:rust test !node_modules", - ]; - - let mut group = c.benchmark_group("realistic_queries"); - for query in queries.iter() { - group.throughput(Throughput::Bytes(query.len() as u64)); - group.bench_with_input(BenchmarkId::from_parameter(query), query, |b, q| { - b.iter(|| parser.parse(black_box(q))); - }); - } - group.finish(); -} - -fn bench_parse_various_lengths(c: &mut Criterion) { - let parser = QueryParser::default(); - - let short = "*.rs"; - let medium = "src name *.rs !test"; - let long = "src lib test name *.rs *.toml !node_modules !test /src/ /lib/ status:modified"; - let very_long = - "a b c d e f g h i j k l m n o p q r s t u v w x y z *.rs *.toml *.md *.txt *.js"; - - let mut group = c.benchmark_group("query_lengths"); - - group.throughput(Throughput::Bytes(short.len() as u64)); - group.bench_with_input(BenchmarkId::new("short", short.len()), &short, |b, q| { - b.iter(|| parser.parse(black_box(q))); - }); - - group.throughput(Throughput::Bytes(medium.len() as u64)); - group.bench_with_input(BenchmarkId::new("medium", medium.len()), &medium, |b, q| { - b.iter(|| parser.parse(black_box(q))); - }); - - group.throughput(Throughput::Bytes(long.len() as u64)); - group.bench_with_input(BenchmarkId::new("long", long.len()), &long, |b, q| { - b.iter(|| parser.parse(black_box(q))); - }); - - group.throughput(Throughput::Bytes(very_long.len() as u64)); - group.bench_with_input( - BenchmarkId::new("very_long", very_long.len()), - &very_long, - |b, q| { - b.iter(|| parser.parse(black_box(q))); - }, - ); - - group.finish(); -} - -fn bench_config_comparison(c: &mut Criterion) { - let file_picker = QueryParser::new(FileSearchConfig); - let grep = QueryParser::new(GrepConfig); - - let query = "src name *.rs !test"; - - let mut group = c.benchmark_group("config_comparison"); - - group.bench_function("file_picker_config", |b| { - b.iter(|| file_picker.parse(black_box(query))); - }); - - group.bench_function("grep_config", |b| { - b.iter(|| grep.parse(black_box(query))); - }); - - group.finish(); -} - -fn bench_constraint_types(c: &mut Criterion) { - let parser = QueryParser::default(); - - let mut group = c.benchmark_group("constraint_types"); - - group.bench_function("extension", |b| { - b.iter(|| parser.parse(black_box("*.rs"))); - }); - - group.bench_function("glob", |b| { - b.iter(|| parser.parse(black_box("**/*.rs"))); - }); - - group.bench_function("exclude", |b| { - b.iter(|| parser.parse(black_box("!test"))); - }); - - group.bench_function("path_segment", |b| { - b.iter(|| parser.parse(black_box("/src/"))); - }); - - group.bench_function("git_status", |b| { - b.iter(|| parser.parse(black_box("status:modified"))); - }); - - group.bench_function("file_type", |b| { - b.iter(|| parser.parse(black_box("type:rust"))); - }); - - group.finish(); -} - -fn bench_worst_case(c: &mut Criterion) { - let parser = QueryParser::default(); - - // Worst case: many constraints that all need to be checked - let worst_case = "a b c d e f g h i j k l m n o p q r s t u v w x y z"; - - c.bench_function("worst_case_many_text_tokens", |b| { - b.iter(|| parser.parse(black_box(worst_case))); - }); - - // Many constraints - let many_constraints = "*.rs *.toml *.md *.txt *.js *.ts *.jsx *.tsx *.vue *.svelte"; - - c.bench_function("worst_case_many_constraints", |b| { - b.iter(|| parser.parse(black_box(many_constraints))); - }); -} - -criterion_group!( - benches, - bench_parse_simple, - bench_parse_complex, - bench_parse_realistic_queries, - bench_parse_various_lengths, - bench_config_comparison, - bench_constraint_types, - bench_worst_case, -); - -criterion_main!(benches); diff --git a/crates/fff-core/src/case_insensitive_memmem.rs b/crates/fff-core/src/case_insensitive_memmem.rs index e0d93a8a..a79ee0c7 100644 --- a/crates/fff-core/src/case_insensitive_memmem.rs +++ b/crates/fff-core/src/case_insensitive_memmem.rs @@ -82,10 +82,18 @@ fn select_rare_pair(needle_lower: &[u8]) -> (usize, usize) { #[inline] fn verify_scalar(h: *const u8, needle_lower: &[u8]) -> bool { - for (i, _) in needle_lower.iter().enumerate() { + verify_scalar_from(h, needle_lower, 0) +} + +/// Byte-by-byte case-folded compare from `i` to the end of the needle. +/// Also serves as the scalar tail of the SIMD verify kernels. +#[inline] +fn verify_scalar_from(h: *const u8, needle_lower: &[u8], mut i: usize) -> bool { + while i < needle_lower.len() { if ascii_fold_byte(unsafe { *h.add(i) }) != needle_lower[i] { return false; } + i += 1; } true } @@ -155,13 +163,7 @@ unsafe fn verify_avx2(h: *const u8, needle_lower: &[u8]) -> bool { } // Scalar tail: handle remaining bytes that don't fill a full 32-byte vector. - while i < len { - if ascii_fold_byte(unsafe { *h.add(i) }) != needle_lower[i] { - return false; - } - i += 1; - } - true + verify_scalar_from(h, needle_lower, i) } // ======== NEON + dotprod (aarch64) =========================================== @@ -240,13 +242,63 @@ unsafe fn verify_neon_dotprod(h: *const u8, needle_lower: &[u8]) -> bool { } // Scalar tail - while i < len { - if ascii_fold_byte(unsafe { *h.add(i) }) != needle_lower[i] { - return false; + verify_scalar_from(h, needle_lower, i) +} + +/// Shared scalar tail for the packed-pair kernels: scan the last few +/// positions that couldn't fill a full vector via memchr on the rarer byte. +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +#[inline(always)] +unsafe fn packed_pair_tail( + haystack: &[u8], + needle_lower: &[u8], + i1: usize, + i2: usize, + offset: usize, + last_start: usize, +) -> bool { + let ptr = haystack.as_ptr(); + if offset <= last_start { + let rare_pos = + if case_insensitive_rank(needle_lower[i1]) <= case_insensitive_rank(needle_lower[i2]) { + i1 + } else { + i2 + }; + let rare_byte = needle_lower[rare_pos]; + let tail_start = offset + rare_pos; + let tail_end = last_start + rare_pos + 1; + if tail_start < tail_end { + let tail_space = &haystack[tail_start..tail_end]; + if rare_byte.is_ascii_lowercase() { + for pos in memchr::memchr2_iter(rare_byte, ascii_swap_case(rare_byte), tail_space) { + let candidate = offset + pos; + if unsafe { verify_dispatch(ptr.add(candidate), needle_lower) } { + return true; + } + } + } else { + for pos in memchr::memchr_iter(rare_byte, tail_space) { + let candidate = offset + pos; + if unsafe { verify_dispatch(ptr.add(candidate), needle_lower) } { + return true; + } + } + } } - i += 1; } - true + false +} + +/// Both case variants of a needle byte for SIMD splatting. +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +#[inline(always)] +fn case_variants(b: u8) -> (u8, u8) { + if b.is_ascii_lowercase() { + (b, ascii_swap_case(b)) + } else { + (b, b) + } } /// NEON packed-pair kernel: scan 16 haystack positions per iteration, @@ -267,18 +319,8 @@ unsafe fn search_packed_pair_neon( let ptr = haystack.as_ptr(); let last_start = hlen - n; - let b1 = needle_lower[i1]; - let b1_alt = if b1.is_ascii_lowercase() { - ascii_swap_case(b1) - } else { - b1 - }; - let b2 = needle_lower[i2]; - let b2_alt = if b2.is_ascii_lowercase() { - ascii_swap_case(b2) - } else { - b2 - }; + let (b1, b1_alt) = case_variants(needle_lower[i1]); + let (b2, b2_alt) = case_variants(needle_lower[i2]); let v1_lo = vdupq_n_u8(b1); let v1_hi = vdupq_n_u8(b1_alt); @@ -314,38 +356,7 @@ unsafe fn search_packed_pair_neon( offset += 16; } - // Tail: remaining positions that couldn't fill a full vector. - if offset <= last_start { - let rare_pos = - if case_insensitive_rank(needle_lower[i1]) <= case_insensitive_rank(needle_lower[i2]) { - i1 - } else { - i2 - }; - let rare_byte = needle_lower[rare_pos]; - let tail_start = offset + rare_pos; - let tail_end = last_start + rare_pos + 1; - if tail_start < tail_end { - let tail_space = &haystack[tail_start..tail_end]; - if rare_byte.is_ascii_lowercase() { - for pos in memchr::memchr2_iter(rare_byte, ascii_swap_case(rare_byte), tail_space) { - let candidate = offset + pos; - if unsafe { verify_dispatch(ptr.add(candidate), needle_lower) } { - return true; - } - } - } else { - for pos in memchr::memchr_iter(rare_byte, tail_space) { - let candidate = offset + pos; - if unsafe { verify_dispatch(ptr.add(candidate), needle_lower) } { - return true; - } - } - } - } - } - - false + unsafe { packed_pair_tail(haystack, needle_lower, i1, i2, offset, last_start) } } #[inline] @@ -387,18 +398,8 @@ unsafe fn search_packed_pair_avx2( let ptr = haystack.as_ptr(); let last_start = hlen - n; // last valid match-start position - let b1 = needle_lower[i1]; - let b1_alt = if b1.is_ascii_lowercase() { - ascii_swap_case(b1) - } else { - b1 - }; - let b2 = needle_lower[i2]; - let b2_alt = if b2.is_ascii_lowercase() { - ascii_swap_case(b2) - } else { - b2 - }; + let (b1, b1_alt) = case_variants(needle_lower[i1]); + let (b2, b2_alt) = case_variants(needle_lower[i2]); let v1_lo = _mm256_set1_epi8(b1 as i8); let v1_hi = _mm256_set1_epi8(b1_alt as i8); @@ -444,39 +445,7 @@ unsafe fn search_packed_pair_avx2( offset += 32; } - // Tail: remaining positions that couldn't fill a full vector. - // Use memchr2 on the rarest byte for these last few positions. - if offset <= last_start { - let rare_pos = - if case_insensitive_rank(needle_lower[i1]) <= case_insensitive_rank(needle_lower[i2]) { - i1 - } else { - i2 - }; - let rare_byte = needle_lower[rare_pos]; - let tail_start = offset + rare_pos; - let tail_end = last_start + rare_pos + 1; - if tail_start < tail_end { - let tail_space = &haystack[tail_start..tail_end]; - if rare_byte.is_ascii_lowercase() { - for pos in memchr::memchr2_iter(rare_byte, ascii_swap_case(rare_byte), tail_space) { - let candidate = offset + pos; - if unsafe { verify_dispatch(ptr.add(candidate), needle_lower) } { - return true; - } - } - } else { - for pos in memchr::memchr_iter(rare_byte, tail_space) { - let candidate = offset + pos; - if unsafe { verify_dispatch(ptr.add(candidate), needle_lower) } { - return true; - } - } - } - } - } - - false + unsafe { packed_pair_tail(haystack, needle_lower, i1, i2, offset, last_start) } } /// Packed-pair case-insensitive substring search.