use aho_corasick::{AhoCorasick, AhoCorasickBuilder, AhoCorasickKind, MatchKind}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use rayon::prelude::*; use rayon::ThreadPoolBuilder; use std::collections::HashMap; static UNICODE_INDEX: &[u8] = include_bytes!("generated/unicode_index.bin"); static UNICODE_POOL: &[u8] = include_bytes!("generated/unicode_pool.bin"); static PYTHON_WHITESPACE: &[u8] = include_bytes!("generated/python_whitespace.bin"); const ALIAS: u8 = 0; const LEXICAL: u8 = 1; const CONJUNCTION: u8 = 2; const FAMILY_NONE: i8 = -1; const FAMILY_SP500: i8 = 0; const FAMILY_NASDAQ100: i8 = 1; const FAMILY_RUSSELL2000: i8 = 2; const FAMILY_MARKET_WIDE: i8 = 3; const ARTICLE_ACCEPTED: u8 = 0; const ARTICLE_REJECTED: u8 = 2; const EXP_SUPPORTED: u8 = 0; const EXP_PLAUSIBLE: u8 = 1; const EXP_NOT_SUPPORTED: u8 = 2; const NORMALIZED_DFA_MAX_PATTERNS: usize = 512; const NORMALIZED_DFA_MAX_PATTERN_BYTES: usize = 16 * 1024; const NORMALIZED_DFA_MAX_HEAP_BYTES: usize = 32 * 1024 * 1024; fn try_standard_automaton( patterns: &[String], ascii_case_insensitive: bool, kind: Option, ) -> Result { if patterns.iter().any(|pattern| pattern.is_empty()) { return Err("patterns must be non-empty".to_string()); } let mut builder = AhoCorasickBuilder::new(); builder .match_kind(MatchKind::Standard) .ascii_case_insensitive(ascii_case_insensitive) .prefilter(true); if let Some(kind) = kind { builder.kind(Some(kind)); } builder.build(patterns).map_err(|error| error.to_string()) } fn build_auto_automaton( patterns: &[String], ascii_case_insensitive: bool, ) -> PyResult { try_standard_automaton(patterns, ascii_case_insensitive, None) .map_err(PyValueError::new_err) } fn normalized_dfa_eligible(patterns: &[String]) -> bool { patterns.len() <= NORMALIZED_DFA_MAX_PATTERNS && patterns .iter() .try_fold(0usize, |total, pattern| total.checked_add(pattern.len())) .is_some_and(|total| total <= NORMALIZED_DFA_MAX_PATTERN_BYTES) } fn build_automaton(patterns: &[String]) -> PyResult { if normalized_dfa_eligible(patterns) { if let Ok(dfa) = try_standard_automaton(patterns, false, Some(AhoCorasickKind::DFA)) { if dfa.memory_usage() <= NORMALIZED_DFA_MAX_HEAP_BYTES { return Ok(dfa); } } } build_auto_automaton(patterns, false) } #[derive(Clone)] struct ConsumerSpec { kind: u8, a: usize, b: usize, c: usize, } #[derive(Clone, Copy, Eq, PartialEq)] enum ExactMode { CaseSensitive, PythonIgnoreCaseAsciiLiteral, } #[derive(Clone)] struct ExactSpec { value: Vec, mode: ExactMode, kind: u8, index: usize, } struct ExactAutomaton { automaton: AhoCorasick, pattern_specs: Vec>, } struct ExactSpecs { specs: Vec, case_sensitive: Option, ignorecase_ascii: Option, } #[derive(Clone, Copy)] struct ExactByteMatch { spec_index: usize, start: usize, end: usize, } #[derive(Clone)] struct ContextRule { window_chars: usize, accept_clauses: Vec>>, reject_terms: Vec>, } #[derive(Clone)] struct AliasSpec { canonical_group: usize, context_rule: Option, valid_from_epoch: i64, valid_to_epoch: Option, family: i8, entity_kind: u8, unresolved_tag: bool, } #[derive(Clone)] struct LexicalSpec { context_window_chars: usize, context_terms: Vec>, is_event: bool, market_wide_authorized: bool, unresolved_tag: bool, has_event_family: bool, } #[derive(Clone)] struct ConjunctionSpec { actor_phrase_count: usize, action_phrase_count: usize, object_phrase_count: usize, scope_phrase_count: usize, } impl ConjunctionSpec { #[inline(always)] fn contains_phrase(&self, field: usize, phrase_index: usize) -> bool { let count = match field { 0 => self.actor_phrase_count, 1 => self.action_phrase_count, 2 => self.object_phrase_count, 3 => self.scope_phrase_count, _ => return false, }; phrase_index < count } } #[derive(Default)] struct ConjunctionMarks { generation: u32, stamps: Vec, masks: Vec, touched: Vec, } impl ConjunctionMarks { fn begin_row(&mut self, rule_count: usize) { if self.stamps.len() < rule_count { self.stamps.resize(rule_count, 0); self.masks.resize(rule_count, 0); } self.touched.clear(); self.generation = self.generation.wrapping_add(1); if self.generation == 0 { self.stamps.fill(0); self.generation = 1; } } #[inline(always)] fn mark(&mut self, rule_index: usize, field: usize) { if rule_index >= self.stamps.len() || field >= 4 { return; } if self.stamps[rule_index] != self.generation { self.stamps[rule_index] = self.generation; self.masks[rule_index] = 0; self.touched.push(rule_index); } self.masks[rule_index] |= 1u8 << field; } #[inline(always)] fn is_complete(&self, rule_index: usize) -> bool { self.stamps[rule_index] == self.generation && self.masks[rule_index] == 0b1111 } #[inline(always)] fn is_empty(&self) -> bool { self.touched.is_empty() } } // (row_index, article_status, sp500, nasdaq100, russell2000, market_wide, // observation_count, native_candidate, exact_candidate) type FactRow = (usize, u8, u8, u8, u8, u8, usize, bool, bool); #[derive(Default)] struct ScanScratch { chars: Vec, normalized: Vec, offsets: Vec, exact_byte_matches: Vec, exact_matches: Vec<(usize, usize, usize)>, exact_boundaries: Vec, alias_candidates: Vec<(usize, usize, usize)>, lexical_candidates: Vec<(usize, usize, usize)>, conjunction_marks: ConjunctionMarks, } impl ScanScratch { fn reset(&mut self, conjunction_count: usize) { self.chars.clear(); self.normalized.clear(); self.offsets.clear(); self.exact_byte_matches.clear(); self.exact_matches.clear(); self.exact_boundaries.clear(); self.alias_candidates.clear(); self.lexical_candidates.clear(); self.conjunction_marks.begin_row(conjunction_count); } } #[inline(always)] fn is_python_ascii_space(byte: u8) -> bool { matches!( byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r' | 0x1c..=0x1f ) } #[inline(always)] fn is_separator_ascii(byte: u8) -> bool { matches!( byte, b'-' | b'.' | b',' | b':' | b';' | b'(' | b')' | b'[' | b']' | b'{' | b'}' | b'/' ) } #[inline(always)] fn is_token_byte(byte: u8) -> bool { byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' } #[inline(always)] fn is_exact_token_char(ch: char, mode: ExactMode) -> bool { if ch.is_ascii_alphanumeric() || ch == '_' { return true; } // Python's Unicode-aware re.IGNORECASE expands [A-Za-z] to these four // additional code points. They are token characters only for the // python_ignorecase_ascii_literal mode; applying this globally would change // the frozen boundary behavior of case-sensitive exact aliases. matches!(mode, ExactMode::PythonIgnoreCaseAsciiLiteral) && matches!(ch, '\u{0130}' | '\u{0131}' | '\u{017f}' | '\u{212a}') } #[inline(always)] fn is_python_space(ch: char) -> bool { let cp = ch as usize; let byte = PYTHON_WHITESPACE[cp >> 3]; (byte & (1 << (cp & 7))) != 0 } #[inline(always)] fn mapped_bytes(ch: char) -> Option<&'static [u8]> { let cp = ch as usize; let base = cp * 4; let offset = u32::from_le_bytes([ UNICODE_INDEX[base], UNICODE_INDEX[base + 1], UNICODE_INDEX[base + 2], UNICODE_INDEX[base + 3], ]) as usize; if offset == 0 { return None; } let len = u16::from_le_bytes([UNICODE_POOL[offset], UNICODE_POOL[offset + 1]]) as usize; Some(&UNICODE_POOL[offset + 2..offset + 2 + len]) } #[inline(always)] fn normalize_post_char( ch: char, source_index: usize, normalized: &mut Vec, offsets: &mut Vec, pending_space: &mut bool, ) { let ch = match ch { '-' | '‐' | '‑' | '‒' | '–' | '—' | '―' | '−' => ' ', '.' | ',' | ':' | ';' | '(' | ')' | '[' | ']' | '{' | '}' | '/' | '/' => ' ', '\'' | '’' | '‘' | '‛' | '`' | '´' => '\'', other => other, }; if is_python_space(ch) { *pending_space = !normalized.is_empty(); return; } if *pending_space { normalized.push(b' '); offsets.push(source_index); *pending_space = false; } let mut buffer = [0u8; 4]; let encoded = ch.encode_utf8(&mut buffer).as_bytes(); normalized.extend_from_slice(encoded); offsets.extend(std::iter::repeat(source_index).take(encoded.len())); } fn normalize_source_char( source_character: char, source_index: usize, normalized: &mut Vec, offsets: &mut Vec, pending_space: &mut bool, ) { if source_character.is_ascii() { let mut byte = (source_character as u8).to_ascii_lowercase(); if is_separator_ascii(byte) { byte = b' '; } else if byte == b'`' { byte = b'\''; } if is_python_ascii_space(byte) { *pending_space = !normalized.is_empty(); return; } if *pending_space { normalized.push(b' '); offsets.push(source_index); *pending_space = false; } normalized.push(byte); offsets.push(source_index); } else if let Some(mapped) = mapped_bytes(source_character) { let mapped = std::str::from_utf8(mapped).expect("generated Unicode pool is valid UTF-8"); for character in mapped.chars() { normalize_post_char(character, source_index, normalized, offsets, pending_space); } } else { normalize_post_char( source_character, source_index, normalized, offsets, pending_space, ); } } #[cfg(test)] fn normalize_chars_into( chars: &[char], source_base: usize, normalized: &mut Vec, offsets: &mut Vec, ) { normalized.clear(); offsets.clear(); normalized.reserve(chars.len()); offsets.reserve(chars.len()); let mut pending_space = false; for (local_index, source_character) in chars.iter().copied().enumerate() { let source_index = source_base + local_index; normalize_source_char( source_character, source_index, normalized, offsets, &mut pending_space, ); } } #[inline(always)] fn normalize_ascii_byte(mut byte: u8) -> u8 { byte.make_ascii_lowercase(); if is_separator_ascii(byte) { b' ' } else if byte == b'`' { b'\'' } else { byte } } fn normalize_ascii_text_into(text: &str, normalized: &mut Vec, offsets: &mut Vec) { debug_assert!(text.is_ascii()); let mut pending_space = false; for (source_index, source_byte) in text.bytes().enumerate() { let byte = normalize_ascii_byte(source_byte); if is_python_ascii_space(byte) { pending_space = !normalized.is_empty(); continue; } if pending_space { normalized.push(b' '); offsets.push(source_index); pending_space = false; } normalized.push(byte); offsets.push(source_index); } } // Phase31A mixed-row block candidate; whole-ASCII control is unchanged. // Derived from a2dbcf9's Phase26 transform and scalar collapse-mask emitter. // The caller retains the frozen Unicode normalizer and whole-ASCII control. #[cfg(target_arch = "x86_64")] fn mixed_ascii_avx2_available() -> bool { static AVAILABLE: std::sync::OnceLock = std::sync::OnceLock::new(); *AVAILABLE.get_or_init(|| std::arch::is_x86_feature_detected!("avx2")) } #[cfg(target_arch = "x86_64")] const MIXED_ASCII_COLLAPSE_BY_LOW_NIBBLE: [u8; 16] = [4, 0, 0, 0, 0, 0, 0, 0, 4, 5, 9, 169, 7, 167, 6, 6]; #[cfg(target_arch = "x86_64")] const MIXED_ASCII_HIGH_NIBBLE_BITS: [u8; 16] = [1, 2, 4, 8, 16, 32, 64, 128, 0, 0, 0, 0, 0, 0, 0, 0]; #[cfg(target_arch = "x86_64")] #[inline(always)] fn emit_mixed_ascii_block( mapped: &[u8], collapse_mask: u32, source_base: usize, normalized: &mut Vec, offsets: &mut Vec, pending_space: &mut bool, ) { debug_assert!(mapped.len() <= 32); let mut local_index = 0usize; while local_index < mapped.len() { let remaining_mask = collapse_mask >> local_index; let remaining_len = mapped.len() - local_index; if remaining_mask & 1 != 0 { let run_len = (remaining_mask.trailing_ones() as usize).min(remaining_len); *pending_space = !normalized.is_empty(); local_index += run_len; continue; } let run_len = (remaining_mask.trailing_zeros() as usize).min(remaining_len); if *pending_space { normalized.push(b' '); offsets.push(source_base + local_index); *pending_space = false; } let run_end = local_index + run_len; normalized.extend_from_slice(&mapped[local_index..run_end]); offsets.extend(source_base + local_index..source_base + run_end); local_index = run_end; } } #[cfg(target_arch = "x86_64")] #[target_feature(enable = "avx2")] unsafe fn normalize_mixed_ascii_prefix_avx2_into( bytes: &[u8], source_base: usize, normalized: &mut Vec, offsets: &mut Vec, pending_space: &mut bool, ) -> usize { use std::arch::x86_64::*; // Every load is within the provided slice. A vector may contain UTF-8, // but only the ASCII prefix before its first high byte may be emitted. // The caller checks AVX2 support before entering this target-feature body. let low_table = _mm256_broadcastsi128_si256(unsafe { _mm_loadu_si128(MIXED_ASCII_COLLAPSE_BY_LOW_NIBBLE.as_ptr().cast()) }); let high_table = _mm256_broadcastsi128_si256(unsafe { _mm_loadu_si128(MIXED_ASCII_HIGH_NIBBLE_BITS.as_ptr().cast()) }); let low_nibble_mask = _mm256_set1_epi8(0x0f); let lowercase_bit = _mm256_set1_epi8(0x20); let upper_lower_bound = _mm256_set1_epi8((b'A' - 1) as i8); let upper_upper_bound = _mm256_set1_epi8((b'Z' + 1) as i8); let backtick = _mm256_set1_epi8(b'`' as i8); let apostrophe = _mm256_set1_epi8(b'\'' as i8); let mut mapped = [0u8; 32]; let mut consumed = 0usize; while bytes.len() - consumed >= 32 { let source = unsafe { _mm256_loadu_si256(bytes.as_ptr().add(consumed).cast()) }; let high_mask = _mm256_movemask_epi8(source) as u32; let ascii_prefix = high_mask.trailing_zeros() as usize; if ascii_prefix == 0 { break; } let low_nibbles = _mm256_and_si256(source, low_nibble_mask); let high_nibbles = _mm256_and_si256(_mm256_srli_epi16(source, 4), low_nibble_mask); let allowed_high_bits = _mm256_shuffle_epi8(low_table, low_nibbles); let observed_high_bits = _mm256_shuffle_epi8(high_table, high_nibbles); let collapse = _mm256_cmpeq_epi8( _mm256_and_si256(allowed_high_bits, observed_high_bits), observed_high_bits, ); let uppercase = _mm256_and_si256( _mm256_cmpgt_epi8(source, upper_lower_bound), _mm256_cmpgt_epi8(upper_upper_bound, source), ); let lowercased = _mm256_or_si256(source, _mm256_and_si256(uppercase, lowercase_bit)); let normalized_backtick = _mm256_blendv_epi8( lowercased, apostrophe, _mm256_cmpeq_epi8(source, backtick), ); unsafe { _mm256_storeu_si256(mapped.as_mut_ptr().cast(), normalized_backtick) }; emit_mixed_ascii_block( &mapped[..ascii_prefix], _mm256_movemask_epi8(collapse) as u32, source_base + consumed, normalized, offsets, pending_space, ); consumed += ascii_prefix; if ascii_prefix < 32 { break; } } consumed } fn normalize_text_into( text: &str, normalized: &mut Vec, offsets: &mut Vec, ) -> (bool, bool, usize) { normalize_text_into_with_mixed_avx2(text, normalized, offsets, true) } fn normalize_text_into_with_mixed_avx2( text: &str, normalized: &mut Vec, offsets: &mut Vec, allow_avx2: bool, ) -> (bool, bool, usize) { normalized.clear(); offsets.clear(); normalized.reserve(text.len()); offsets.reserve(text.len()); if text.is_ascii() { normalize_ascii_text_into(text, normalized, offsets); return (true, false, text.len()); } // Keep the whole-ASCII path above unchanged. Feature detection is cached // and selected once per mixed row, never without the runtime guard. #[cfg(target_arch = "x86_64")] let use_avx2 = allow_avx2 && mixed_ascii_avx2_available(); #[cfg(not(target_arch = "x86_64"))] let _ = allow_avx2; let bytes = text.as_bytes(); let mut pending_space = false; let mut exceptional_ignorecase = false; let mut byte_cursor = 0usize; let mut source_len = 0usize; while byte_cursor < bytes.len() { #[cfg(target_arch = "x86_64")] if use_avx2 && bytes.len() - byte_cursor >= 32 && bytes[byte_cursor].is_ascii() { // SAFETY: use_avx2 includes runtime feature detection. The helper // bounds every load and emits only bytes before the first high bit. let consumed = unsafe { normalize_mixed_ascii_prefix_avx2_into( &bytes[byte_cursor..], source_len, normalized, offsets, &mut pending_space, ) }; byte_cursor += consumed; source_len += consumed; // Every consumed byte is one ASCII scalar. } // Consume raw ASCII tails once, without decoding their bytes as Unicode // scalars or searching the complete run before processing it. Original // code-point positions remain separate from UTF-8 byte positions. while byte_cursor < bytes.len() && bytes[byte_cursor].is_ascii() { let byte = normalize_ascii_byte(bytes[byte_cursor]); if is_python_ascii_space(byte) { pending_space = !normalized.is_empty(); } else { if pending_space { normalized.push(b' '); offsets.push(source_len); pending_space = false; } normalized.push(byte); offsets.push(source_len); } byte_cursor += 1; source_len += 1; } if byte_cursor == bytes.len() { break; } // The cursor starts at a character boundary and advances by one byte // for ASCII or by the complete decoded scalar's UTF-8 width. Keep the // frozen per-scalar mapping and repeated expansion offsets unchanged. let source_character = text[byte_cursor..] .chars() .next() .expect("remaining input contains a complete UTF-8 scalar"); exceptional_ignorecase |= matches!( source_character, '\u{0130}' | '\u{0131}' | '\u{017f}' | '\u{212a}' ); normalize_source_char( source_character, source_len, normalized, offsets, &mut pending_space, ); byte_cursor += source_character.len_utf8(); source_len += 1; } // The unchanged whole-row ASCII gate already rejected this input. (false, exceptional_ignorecase, source_len) } #[inline] fn normalized_contains(haystack: &[u8], needle: &[u8]) -> bool { if needle.is_empty() || haystack.len() < needle.len() { return false; } let mut cursor = 0usize; while cursor + needle.len() <= haystack.len() { if let Some(relative) = haystack[cursor..] .windows(needle.len()) .position(|window| window == needle) { let start = cursor + relative; let end = start + needle.len(); let left_ok = start == 0 || !is_token_byte(haystack[start - 1]); let right_ok = end == haystack.len() || !is_token_byte(haystack[end]); if left_ok && right_ok { return true; } cursor = start + 1; } else { return false; } } false } fn normalized_context_window<'a>( normalized: &'a [u8], offsets: &[usize], source_len: usize, start: usize, end: usize, width: usize, ) -> &'a [u8] { debug_assert_eq!(normalized.len(), offsets.len()); debug_assert!(start <= end); debug_assert!(end <= source_len); let left = start.saturating_sub(width); let right = end.saturating_add(width).min(source_len); let normalized_left = offsets.partition_point(|&source_index| source_index < left); let normalized_right = offsets.partition_point(|&source_index| source_index < right); let mut window = &normalized[normalized_left..normalized_right]; // A full-row normalization can carry a collapsed separator from before the // source window to its first non-separator. Normalizing the source window in // isolation drops that leading separator, so remove it from the borrowed // slice as well. Trailing separators already map to the first source // character after the window and are excluded by normalized_right. if window.first() == Some(&b' ') { window = &window[1..]; } window } fn context_status( normalized: &[u8], offsets: &[usize], source_len: usize, start: usize, end: usize, rule: &ContextRule, ) -> u8 { // 0=satisfied, 1=explicit_collision_exclusion, 2=context_clause_not_satisfied let window = normalized_context_window( normalized, offsets, source_len, start, end, rule.window_chars, ); if rule .reject_terms .iter() .any(|term| normalized_contains(window, term)) { return 1; } if rule .accept_clauses .iter() .any(|clause| clause.iter().all(|term| normalized_contains(window, term))) { 0 } else { 2 } } #[inline(always)] fn py_ignorecase_ascii_literal_eq(pattern: char, observed: char) -> bool { let lower = pattern.to_ascii_lowercase(); if observed.is_ascii() { return observed.to_ascii_lowercase() == lower; } match lower { 'i' => matches!(observed, 'İ' | 'ı'), 's' => observed == 'ſ', 'k' => observed == 'K', _ => false, } } fn exact_matches(chars: &[char], spec: &ExactSpec) -> Vec<(usize, usize)> { let width = spec.value.len(); if width == 0 || width > chars.len() { return Vec::new(); } let mut output = Vec::new(); for start in 0..=chars.len() - width { let end = start + width; if start > 0 && is_exact_token_char(chars[start - 1], spec.mode) { continue; } if end < chars.len() && is_exact_token_char(chars[end], spec.mode) { continue; } let matched = spec.value.iter().enumerate().all(|(offset, expected)| { let observed = chars[start + offset]; match spec.mode { ExactMode::CaseSensitive => observed == *expected, ExactMode::PythonIgnoreCaseAsciiLiteral => { py_ignorecase_ascii_literal_eq(*expected, observed) } } }); if matched { output.push((start, end)); } } output } #[inline(always)] fn exact_boundaries_match(text: &str, start: usize, end: usize, mode: ExactMode) -> bool { let left_ok = start == 0 || !is_exact_token_char( text[..start] .chars() .next_back() .expect("nonempty exact left boundary"), mode, ); let right_ok = end == text.len() || !is_exact_token_char( text[end..] .chars() .next() .expect("nonempty exact right boundary"), mode, ); left_ok && right_ok } impl ExactAutomaton { fn append_matches(&self, text: &str, mode: ExactMode, output: &mut Vec) { for matched in self.automaton.find_overlapping_iter(text.as_bytes()) { let start = matched.start(); let end = matched.end(); if !exact_boundaries_match(text, start, end, mode) { continue; } for &spec_index in &self.pattern_specs[matched.pattern().as_usize()] { output.push(ExactByteMatch { spec_index, start, end, }); } } } } fn build_exact_automaton( specs: &[ExactSpec], mode: ExactMode, ascii_case_insensitive: bool, ) -> PyResult> { let mut patterns = Vec::::new(); let mut pattern_specs = Vec::>::new(); let mut pattern_ids = HashMap::::new(); for (spec_index, spec) in specs.iter().enumerate() { if spec.mode != mode { continue; } let value: String = spec.value.iter().collect(); let pattern_key = if ascii_case_insensitive { value.to_ascii_lowercase() } else { value.clone() }; if let Some(&pattern_id) = pattern_ids.get(&pattern_key) { pattern_specs[pattern_id].push(spec_index); } else { let pattern_id = patterns.len(); pattern_ids.insert(pattern_key, pattern_id); patterns.push(value); pattern_specs.push(vec![spec_index]); } } if patterns.is_empty() { return Ok(None); } let automaton = build_auto_automaton(&patterns, ascii_case_insensitive)?; Ok(Some(ExactAutomaton { automaton, pattern_specs, })) } impl ExactSpecs { fn new(specs: Vec) -> PyResult { let case_sensitive = build_exact_automaton(&specs, ExactMode::CaseSensitive, false)?; let ignorecase_ascii = build_exact_automaton(&specs, ExactMode::PythonIgnoreCaseAsciiLiteral, true)?; Ok(Self { specs, case_sensitive, ignorecase_ascii, }) } fn append_matching( &self, text: &str, byte_matches: &mut Vec, output: &mut Vec<(usize, usize, usize)>, exceptional_chars: &mut Vec, boundaries: &mut Vec, text_is_ascii: bool, exceptional_ignorecase: bool, ) { byte_matches.clear(); output.clear(); exceptional_chars.clear(); boundaries.clear(); if let Some(automaton) = &self.case_sensitive { automaton.append_matches(text, ExactMode::CaseSensitive, byte_matches); } if !exceptional_ignorecase { if let Some(automaton) = &self.ignorecase_ascii { automaton.append_matches( text, ExactMode::PythonIgnoreCaseAsciiLiteral, byte_matches, ); } } output.reserve(byte_matches.len()); if text_is_ascii { output.extend( byte_matches .iter() .copied() .map(|matched| (matched.spec_index, matched.start, matched.end)), ); } else if !byte_matches.is_empty() { // Aho-Corasick reports UTF-8 byte offsets while the frozen Python // surface reports code-point offsets. Build this map only for the // uncommon non-ASCII row that actually produced an exact hit. boundaries.extend(text.char_indices().map(|(offset, _)| offset)); boundaries.push(text.len()); output.extend(byte_matches.iter().copied().map(|matched| { let start = boundaries .binary_search(&matched.start) .expect("exact match starts at a UTF-8 character boundary"); let end = boundaries .binary_search(&matched.end) .expect("exact match ends at a UTF-8 character boundary"); (matched.spec_index, start, end) })); } if exceptional_ignorecase { // Python re.IGNORECASE gives ASCII I/S/K four additional Unicode // equivalents (İ, ı, ſ, K). The common ASCII automaton cannot change // byte width while matching, so retain the exact scalar fallback // only for rows that contain one of those exceptional code points. exceptional_chars.extend(text.chars()); for (spec_index, spec) in self.specs.iter().enumerate() { if spec.mode != ExactMode::PythonIgnoreCaseAsciiLiteral { continue; } output.extend( exact_matches(exceptional_chars, spec) .into_iter() .map(|(start, end)| (spec_index, start, end)), ); } } } #[cfg(test)] fn matching(&self, text: &str) -> Vec<(usize, usize, usize)> { let mut byte_matches = Vec::new(); let mut output = Vec::new(); let mut exceptional_chars = Vec::new(); let mut boundaries = Vec::new(); let mut normalized = Vec::new(); let mut offsets = Vec::new(); let (text_is_ascii, exceptional_ignorecase, _) = normalize_text_into(text, &mut normalized, &mut offsets); self.append_matching( text, &mut byte_matches, &mut output, &mut exceptional_chars, &mut boundaries, text_is_ascii, exceptional_ignorecase, ); output } } fn scan_one( automaton: &AhoCorasick, pattern_consumers: &[Vec], consumers: &[ConsumerSpec], exact_specs: &ExactSpecs, aliases: &[AliasSpec], lexical: &[LexicalSpec], contexts: &[ContextRule], conjunctions: &[ConjunctionSpec], text: &str, observed_epoch: i64, row_index: usize, scratch: &mut ScanScratch, ) -> Option { scratch.reset(conjunctions.len()); let (text_is_ascii, exceptional_ignorecase, source_len) = normalize_text_into(text, &mut scratch.normalized, &mut scratch.offsets); let mut native_candidate = false; if !scratch.normalized.is_empty() { for matched in automaton.find_overlapping_iter(&scratch.normalized) { let start = matched.start(); let end = matched.end(); let left_ok = start == 0 || !is_token_byte(scratch.normalized[start - 1]); let right_ok = end == scratch.normalized.len() || !is_token_byte(scratch.normalized[end]); if !left_ok || !right_ok { continue; } native_candidate = true; let original_start = scratch.offsets[start]; let original_end = scratch.offsets[end - 1] + 1; for &consumer_id in &pattern_consumers[matched.pattern().as_usize()] { let spec = &consumers[consumer_id]; match spec.kind { ALIAS => { scratch .alias_candidates .push((spec.a, original_start, original_end)); } LEXICAL => { scratch .lexical_candidates .push((spec.a, original_start, original_end)); } CONJUNCTION => { if conjunctions .get(spec.a) .is_some_and(|rule| rule.contains_phrase(spec.b, spec.c)) { scratch.conjunction_marks.mark(spec.a, spec.b); } } _ => unreachable!("validated consumer kind"), } } } } let mut exact_candidate = false; exact_specs.append_matching( text, &mut scratch.exact_byte_matches, &mut scratch.exact_matches, &mut scratch.chars, &mut scratch.exact_boundaries, text_is_ascii, exceptional_ignorecase, ); for &(spec_index, start, end) in &scratch.exact_matches { exact_candidate = true; let spec = &exact_specs.specs[spec_index]; match spec.kind { ALIAS => scratch.alias_candidates.push((spec.index, start, end)), LEXICAL => scratch.lexical_candidates.push((spec.index, start, end)), CONJUNCTION => { scratch.conjunction_marks.mark(spec.index, 0); } _ => unreachable!("validated exact kind"), } } if scratch.alias_candidates.is_empty() && scratch.lexical_candidates.is_empty() && scratch.conjunction_marks.is_empty() { return None; } let mut accepted_nonsemantic = false; let mut accepted_evidence = false; let mut unresolved_exposure = false; let mut unresolved_event = false; let mut supported_mask: u8 = 0; let mut observation_count = 0usize; // Group aliases by (canonical_id, start) without a per-row hash table. The // longest end in each group wins, while distinct aliases that tie remain // distinct observations. Exact duplicate candidates collapse here. scratch.alias_candidates.sort_unstable_by_key( |&(alias_index, start, end)| { (aliases[alias_index].canonical_group, start, end, alias_index) }, ); scratch.alias_candidates.dedup(); let mut group_start = 0usize; while group_start < scratch.alias_candidates.len() { let (first_alias, first_start, _) = scratch.alias_candidates[group_start]; let canonical_group = aliases[first_alias].canonical_group; let mut group_end = group_start + 1; let mut longest_end = scratch.alias_candidates[group_start].2; while group_end < scratch.alias_candidates.len() { let (alias_index, start, end) = scratch.alias_candidates[group_end]; if aliases[alias_index].canonical_group != canonical_group || start != first_start { break; } longest_end = longest_end.max(end); group_end += 1; } for candidate_index in group_start..group_end { let (alias_index, start, end) = scratch.alias_candidates[candidate_index]; if end < longest_end { continue; } let spec = &aliases[alias_index]; // The frozen compact contract counts a retained observation even // when its temporal or context checks later reject it. observation_count += 1; let active = observed_epoch >= spec.valid_from_epoch && spec .valid_to_epoch .map(|until| observed_epoch < until) .unwrap_or(true); if !active { continue; } let context_code = if let Some(rule_index) = spec.context_rule { context_status( &scratch.normalized, &scratch.offsets, source_len, start, end, &contexts[rule_index], ) } else { 0 }; if context_code != 0 { // Frozen exposure unresolved filter excludes explicit collision and // context-clause failures. Missing context cannot occur because the // validated config rejects unknown rules during construction. continue; } accepted_nonsemantic = true; if spec.family != FAMILY_NONE && (spec.entity_kind == 0 || spec.entity_kind == 1) { accepted_evidence = true; supported_mask |= 1u8 << (spec.family as u8); } else if spec.unresolved_tag { unresolved_exposure = true; } } group_start = group_end; } // Observation identity for lexical matches is (rule, start, end), so the // sorted sparse candidates replace the former observation HashSet exactly. scratch.lexical_candidates.sort_unstable(); scratch.lexical_candidates.dedup(); for candidate_index in 0..scratch.lexical_candidates.len() { let (lexical_index, start, end) = scratch.lexical_candidates[candidate_index]; let spec = &lexical[lexical_index]; observation_count += 1; let context_ok = if spec.context_terms.is_empty() { true } else { let window = normalized_context_window( &scratch.normalized, &scratch.offsets, source_len, start, end, spec.context_window_chars, ); spec.context_terms .iter() .any(|term| normalized_contains(window, term)) }; if spec.is_event { if spec.market_wide_authorized { accepted_evidence = true; supported_mask |= 1u8 << FAMILY_MARKET_WIDE; } else { if spec.unresolved_tag { unresolved_exposure = true; } if spec.has_event_family { unresolved_event = true; } } } else { if context_ok { accepted_nonsemantic = true; } if spec.market_wide_authorized && context_ok { accepted_evidence = true; supported_mask |= 1u8 << FAMILY_MARKET_WIDE; } else if spec.unresolved_tag { // Matches frozen generic lexical unresolved branch, including a // lexical context failure, whose evidence reason is the generic // identity-resolved-without-path code. unresolved_exposure = true; } } } for touched_index in 0..scratch.conjunction_marks.touched.len() { let rule_index = scratch.conjunction_marks.touched[touched_index]; if scratch.conjunction_marks.is_complete(rule_index) { // Only one conjunction observation exists per configured rule. observation_count += 1; accepted_evidence = true; supported_mask |= 1u8 << FAMILY_MARKET_WIDE; } } if observation_count == 0 { return None; } let article_status = if accepted_evidence || accepted_nonsemantic || unresolved_event { ARTICLE_ACCEPTED } else { ARTICLE_REJECTED }; let completed_elsewhere = supported_mask != 0; let exposure_status = |family: i8| -> u8 { if (supported_mask & (1u8 << (family as u8))) != 0 { EXP_SUPPORTED } else if family != FAMILY_MARKET_WIDE && !completed_elsewhere && unresolved_exposure { EXP_PLAUSIBLE } else { EXP_NOT_SUPPORTED } }; Some(( row_index, article_status, exposure_status(FAMILY_SP500), exposure_status(FAMILY_NASDAQ100), exposure_status(FAMILY_RUSSELL2000), exposure_status(FAMILY_MARKET_WIDE), observation_count, native_candidate, exact_candidate, )) } #[pyclass(frozen, module = "range_curate._native")] struct NativeCompactFactScanner { automaton: AhoCorasick, pattern_consumers: Vec>, consumers: Vec, exact_specs: ExactSpecs, aliases: Vec, lexical: Vec, contexts: Vec, conjunctions: Vec, } #[pymethods] impl NativeCompactFactScanner { #[new] #[pyo3(signature = ( patterns, pattern_consumers, consumers, exact_specs, alias_specs, lexical_specs, context_rules, conjunction_specs ))] fn new( patterns: Vec, pattern_consumers: Vec>, consumers: Vec<(u8, usize, usize, usize)>, exact_specs: Vec<(String, String, u8, usize)>, alias_specs: Vec<(usize, i64, Option, i8, u8, bool, Option)>, lexical_specs: Vec<(usize, Vec, bool, bool, bool, bool)>, context_rules: Vec<(usize, Vec>, Vec)>, conjunction_specs: Vec<(usize, usize, usize, usize)>, ) -> PyResult { if patterns.len() != pattern_consumers.len() { return Err(PyValueError::new_err("pattern_consumers length mismatch")); } let automaton = build_automaton(&patterns)?; let consumers = consumers .into_iter() .map(|(kind, a, b, c)| ConsumerSpec { kind, a, b, c }) .collect(); let mut parsed_exact = Vec::with_capacity(exact_specs.len()); for (value, mode, kind, index) in exact_specs { if value.is_empty() { return Err(PyValueError::new_err("exact value must be non-empty")); } let mode = match mode.as_str() { "case_sensitive" => ExactMode::CaseSensitive, "python_ignorecase_ascii_literal" => { if !value.is_ascii() { return Err(PyValueError::new_err("ignorecase literal must be ASCII")); } ExactMode::PythonIgnoreCaseAsciiLiteral } _ => return Err(PyValueError::new_err(format!("unknown exact mode: {mode}"))), }; parsed_exact.push(ExactSpec { value: value.chars().collect(), mode, kind, index, }); } let aliases = alias_specs .into_iter() .map( |( canonical_group, valid_from_epoch, valid_to_epoch, family, entity_kind, unresolved_tag, context_rule, )| AliasSpec { canonical_group, valid_from_epoch, valid_to_epoch, family, entity_kind, unresolved_tag, context_rule, }, ) .collect(); let lexical = lexical_specs .into_iter() .map( |( context_window_chars, terms, is_event, market_wide_authorized, unresolved_tag, has_event_family, )| LexicalSpec { context_window_chars, context_terms: terms.into_iter().map(String::into_bytes).collect(), is_event, market_wide_authorized, unresolved_tag, has_event_family, }, ) .collect(); let contexts = context_rules .into_iter() .map(|(window_chars, clauses, rejects)| ContextRule { window_chars, accept_clauses: clauses .into_iter() .map(|clause| clause.into_iter().map(String::into_bytes).collect()) .collect(), reject_terms: rejects.into_iter().map(String::into_bytes).collect(), }) .collect(); let conjunctions = conjunction_specs .into_iter() .map( |( actor_phrase_count, action_phrase_count, object_phrase_count, scope_phrase_count, )| ConjunctionSpec { actor_phrase_count, action_phrase_count, object_phrase_count, scope_phrase_count, }, ) .collect(); let exact_specs = ExactSpecs::new(parsed_exact)?; Ok(Self { automaton, pattern_consumers, consumers, exact_specs, aliases, lexical, contexts, conjunctions, }) } #[pyo3(signature = (texts, epochs, workers=4))] fn screen_facts_batch( &self, py: Python<'_>, texts: Vec, epochs: Vec, workers: usize, ) -> PyResult> { if texts.len() != epochs.len() { return Err(PyValueError::new_err("texts/epochs length mismatch")); } if workers == 0 { return Err(PyValueError::new_err("workers must be >= 1")); } let pool = ThreadPoolBuilder::new() .num_threads(workers) .thread_name(|index| format!("news-relevance-worker-{index}")) .build() .map_err(|error| PyValueError::new_err(error.to_string()))?; let automaton = &self.automaton; let pattern_consumers = &self.pattern_consumers; let consumers = &self.consumers; let exact_specs = &self.exact_specs; let aliases = &self.aliases; let lexical = &self.lexical; let contexts = &self.contexts; let conjunctions = &self.conjunctions; Ok(py.detach(move || { pool.install(|| { texts .par_iter() .zip(epochs.par_iter()) .enumerate() .map_init(ScanScratch::default, |scratch, (index, (text, epoch))| { scan_one( automaton, pattern_consumers, consumers, exact_specs, aliases, lexical, contexts, conjunctions, text, *epoch, index, scratch, ) }) .filter_map(|row| row) .collect() }) })) } } #[cfg(test)] mod matcher_tests { use super::*; fn spec(value: &str, mode: ExactMode, index: usize) -> ExactSpec { ExactSpec { value: value.chars().collect(), mode, kind: ALIAS, index, } } fn synthetic_patterns(count: usize) -> Vec { (0..count) .map(|index| format!("synthetic pattern {index:04}")) .collect() } fn overlapping_matches(automaton: &AhoCorasick, text: &str) -> Vec<(usize, usize, usize)> { let mut matches: Vec<_> = automaton .find_overlapping_iter(text.as_bytes()) .map(|matched| { ( matched.pattern().as_usize(), matched.start(), matched.end(), ) }) .collect(); matches.sort_unstable(); matches } #[test] fn default_sized_normalized_taxonomy_selects_bounded_dfa() { // The maintained taxonomy currently has 226 normalized patterns, so // this sits above aho-corasick's automatic DFA threshold while still // remaining comfortably inside RANGE's explicit safety bounds. let patterns = synthetic_patterns(226); assert!(normalized_dfa_eligible(&patterns)); let automaton = build_automaton(&patterns).expect("bounded DFA builds"); assert_eq!(automaton.kind(), AhoCorasickKind::DFA); assert!(automaton.memory_usage() <= NORMALIZED_DFA_MAX_HEAP_BYTES); } #[test] fn large_custom_taxonomy_falls_back_to_ordinary_auto_selection() { let patterns = synthetic_patterns(NORMALIZED_DFA_MAX_PATTERNS + 1); assert!(!normalized_dfa_eligible(&patterns)); let selected = build_automaton(&patterns).expect("fallback automaton builds"); let automatic = build_auto_automaton(&patterns, false).expect("auto automaton builds"); assert_eq!(selected.kind(), automatic.kind()); assert_ne!(selected.kind(), AhoCorasickKind::DFA); } #[test] fn bounded_dfa_preserves_standard_overlapping_matches() { let mut patterns = synthetic_patterns(101); patterns.extend([ "equity market".to_string(), "equity market selloff".to_string(), "market selloff".to_string(), "capital rule".to_string(), "rule".to_string(), ]); let text = "a capital rule preceded an equity market selloff"; let selected = build_automaton(&patterns).expect("bounded DFA builds"); let automatic = build_auto_automaton(&patterns, false).expect("auto automaton builds"); assert_eq!(selected.kind(), AhoCorasickKind::DFA); assert_ne!(automatic.kind(), AhoCorasickKind::DFA); assert_eq!(selected.match_kind(), MatchKind::Standard); assert_eq!( overlapping_matches(&selected, text), overlapping_matches(&automatic, text) ); } #[test] fn exact_automata_equal_scalar_reference_across_boundaries_and_unicode() { let specs = vec![ spec("UPRO", ExactMode::CaseSensitive, 0), spec("QQQ", ExactMode::PythonIgnoreCaseAsciiLiteral, 1), spec("iShares", ExactMode::PythonIgnoreCaseAsciiLiteral, 2), spec("PowerShares", ExactMode::PythonIgnoreCaseAsciiLiteral, 3), // Duplicate literals must still fan out to every configured consumer. spec("SEC", ExactMode::CaseSensitive, 4), spec("SEC", ExactMode::CaseSensitive, 5), ]; let matcher = ExactSpecs::new(specs).expect("valid exact automata"); let texts = [ "UPRO upro QQQ qqq xQQQ QQQx SEC", "é UPRO αQQQ αSEC", "İShares ıSHARES iShares Powerſhares", "İQQQ QQQİ ſQQQ QQQK", "no exact literals here", ]; for text in texts { let chars: Vec = text.chars().collect(); let mut expected = Vec::new(); for (spec_index, spec) in matcher.specs.iter().enumerate() { expected.extend( exact_matches(&chars, spec) .into_iter() .map(|(start, end)| (spec_index, start, end)), ); } expected.sort_unstable(); let mut observed = matcher.matching(text); observed.sort_unstable(); assert_eq!(observed, expected, "text={text:?}"); assert_eq!(!observed.is_empty(), !expected.is_empty()); } } #[test] fn python_ignorecase_exceptions_remain_token_boundaries() { let matcher = ExactSpecs::new(vec![spec( "QQQ", ExactMode::PythonIgnoreCaseAsciiLiteral, 0, )]) .expect("valid exact automaton"); for text in ["İQQQ", "QQQı", "ſQQQ", "QQQK"] { assert!(matcher.matching(text).is_empty(), "text={text:?}"); } } #[test] fn direct_text_normalization_matches_character_slice_reference() { let texts = [ "ASCII -- punctuation\tand spaces", "Café — ‘quoted’ / fullwidth", "Straße İ ı ſ K fi", "\u{001c}leading\u{00a0}and\u{2029}trailing\n", "", ]; for text in texts { let chars: Vec = text.chars().collect(); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); normalize_chars_into(&chars, 0, &mut expected, &mut expected_offsets); let mut observed = Vec::new(); let mut observed_offsets = Vec::new(); let (all_ascii, exceptional_ignorecase, source_len) = normalize_text_into(text, &mut observed, &mut observed_offsets); assert_eq!(observed, expected, "text={text:?}"); assert_eq!(observed_offsets, expected_offsets, "text={text:?}"); assert_eq!(source_len, chars.len(), "text={text:?}"); assert_eq!( all_ascii, chars.iter().all(|character| character.is_ascii()) ); assert_eq!( exceptional_ignorecase, chars.iter().any(|character| matches!( character, '\u{0130}' | '\u{0131}' | '\u{017f}' | '\u{212a}' )), "text={text:?}" ); } } // Reuse all reference/candidate buffers across exhaustive synthetic cases. // This keeps auxiliary memory bounded by the longest individual fixture. fn normalization_reference_checker() -> impl FnMut(&str) { let mut chars = Vec::new(); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); let mut observed = Vec::new(); let mut observed_offsets = Vec::new(); move |text: &str| { chars.clear(); chars.extend(text.chars()); normalize_chars_into(&chars, 0, &mut expected, &mut expected_offsets); // The existing exhaustive scalar and boundary fixtures explicitly // exercise both forced scalar and runtime-guarded vector dispatch. // On non-AVX2 machines both are portable scalar; vector status must // be reported as unexercised, not counted as a passing vector gate. for allow_avx2 in [false, true] { let flags = normalize_text_into_with_mixed_avx2( text, &mut observed, &mut observed_offsets, allow_avx2, ); assert_eq!(observed, expected, "allow_avx2={allow_avx2} text={text:?}"); assert_eq!(observed_offsets, expected_offsets, "allow_avx2={allow_avx2} text={text:?}"); assert_eq!( flags, ( text.is_ascii(), chars.iter().any(|character| matches!( character, '\u{0130}' | '\u{0131}' | '\u{017f}' | '\u{212a}' )), chars.len(), ), "allow_avx2={allow_avx2} text={text:?}", ); } } } #[test] fn mixed_ascii_runs_match_reference_for_every_valid_scalar() { let mut check = normalization_reference_checker(); // Reuse one bounded buffer. Each non-ASCII scalar has a complete // vector block on both sides; ASCII scalars occur inside a vector block. // NBSP keeps every scalar fixture on the mixed-row branch. Both forced // scalar and guarded vector modes run in the shared reference checker. let prefix = "A \t-B`/ ".repeat(4); let suffix = " /C D`B ".repeat(4); assert_eq!(prefix.len(), 32); assert_eq!(suffix.len(), 32); let mut text = String::with_capacity(prefix.len() + suffix.len() + 8); // One fixture per valid scalar, not a scalar/run-length Cartesian product. for codepoint in 0..=0x10ffff { let Some(character) = char::from_u32(codepoint) else { continue; }; text.clear(); text.push_str(&prefix); text.push(character); text.push_str(&suffix); text.push_str("\u{00a0}D"); check(&text); } } #[test] fn mixed_ascii_runs_preserve_all_ascii_bytes_at_unicode_boundaries() { let mut check = normalization_reference_checker(); let mut text = String::with_capacity(160); let unicode = [ '\u{0080}', 'é', '\u{0301}', '—', '\u{00a0}', '…', '\u{2029}', 'fi', 'İ', 'ı', 'ſ', 'K', '界', '🦀', '\u{fdfa}', '\u{10ffff}', ]; for run_length in [0, 1, 15, 16, 31, 32, 33, 63, 64, 65, 127, 128, 129] { for byte in 0u8..=0x7f { for character in unicode { text.clear(); text.extend(std::iter::repeat('A').take(run_length)); text.push(byte as char); text.push(character); text.push(byte as char); text.push_str(" \tB/"); check(&text); // Also cover a Unicode scalar first and last, with the // same ASCII byte on both sides of a variable-length run. text.clear(); text.push(character); text.push(byte as char); text.extend(std::iter::repeat('Z').take(run_length)); text.push(byte as char); text.push(character); check(&text); } } } } #[test] fn mixed_ascii_runs_keep_pending_space_and_expansion_source_offsets() { let mut normalized = Vec::new(); let mut offsets = Vec::new(); assert_eq!( normalize_text_into("\u{00a0}\u{001c}É — \tB", &mut normalized, &mut offsets), (false, false, 8), ); assert_eq!(normalized, "é b".as_bytes()); assert_eq!(offsets, [2, 2, 7, 7]); assert_eq!( normalize_text_into("A\u{00a0}fi\u{001c}B", &mut normalized, &mut offsets), (false, false, 5), ); assert_eq!(normalized, b"a fi b"); assert_eq!(offsets, [0, 2, 2, 2, 4, 4]); let mut check = normalization_reference_checker(); for text in [ "éα界🦀", "İIıſK", "\u{00a0}\u{2029}—", "A\u{0301}B", "\u{0301}A\u{0301}", "A—\u{00a0}\u{001c}B", "A\u{fdfa}B", ] { check(text); } } fn assert_ascii_normalization_matches_character_reference(bytes: &[u8]) { let text = std::str::from_utf8(bytes).expect("ASCII bytes are valid UTF-8"); let chars: Vec = text.chars().collect(); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); normalize_chars_into(&chars, 0, &mut expected, &mut expected_offsets); let mut observed = Vec::new(); let mut observed_offsets = Vec::new(); let flags = normalize_text_into(text, &mut observed, &mut observed_offsets); assert_eq!(flags, (true, false, bytes.len()), "bytes={bytes:?}"); assert_eq!(observed, expected, "bytes={bytes:?}"); assert_eq!(observed_offsets, expected_offsets, "bytes={bytes:?}"); } #[test] fn ascii_byte_path_matches_character_reference_exhaustively() { assert_ascii_normalization_matches_character_reference(b""); for first in 0u8..=0x7f { assert_ascii_normalization_matches_character_reference(&[first]); assert_ascii_normalization_matches_character_reference(&[b'x', first, b'y']); for second in 0u8..=0x7f { assert_ascii_normalization_matches_character_reference(&[first, second]); assert_ascii_normalization_matches_character_reference(&[ b'x', first, second, b'y', ]); } } } #[test] fn ascii_byte_path_preserves_collapse_and_control_semantics() { let tricky = [ b' ', b'-', b'\t', b'A', b'`', b'B', b'.', b'/', b' ', 0x00, b'\\', 0x7f, b' ', b'\n', ]; assert_ascii_normalization_matches_character_reference(&tricky); let mut normalized = Vec::new(); let mut offsets = Vec::new(); let text = std::str::from_utf8(&tricky).expect("ASCII bytes are valid UTF-8"); assert_eq!( normalize_text_into(text, &mut normalized, &mut offsets), (true, false, tricky.len()) ); assert_eq!(normalized, b"a'b \0\\\x7f"); assert_eq!(offsets, [3, 4, 5, 9, 9, 10, 11]); let python_ascii_whitespace = [ b' ', b'\t', b'\n', 0x0b, 0x0c, b'\r', 0x1c, 0x1d, 0x1e, 0x1f, ]; let separators = [ b'-', b'.', b',', b':', b';', b'(', b')', b'[', b']', b'{', b'}', b'/', ]; let mut runs = vec![b'A']; runs.extend_from_slice(&python_ascii_whitespace); runs.extend_from_slice(&separators); runs.push(b'Z'); assert_ascii_normalization_matches_character_reference(&runs); normalize_text_into( std::str::from_utf8(&runs).expect("ASCII bytes are valid UTF-8"), &mut normalized, &mut offsets, ); assert_eq!(normalized, b"a z"); assert_eq!(offsets, [0, runs.len() - 1, runs.len() - 1]); } #[test] fn borrowed_context_windows_match_isolated_normalization_exhaustively() { let texts = [ "", "a", " --Federal\tReserve...cut rates ", "prefix---GDP\u{00a0}growth/suffix", "Café — ‘SEC’ / filing", "İ ı ſ K fi — SPY ETF", "x\u{001c}\u{001d}market\u{2029}structure\n", ]; for text in texts { let chars: Vec = text.chars().collect(); let mut normalized = Vec::new(); let mut offsets = Vec::new(); let (_, _, source_len) = normalize_text_into(text, &mut normalized, &mut offsets); assert_eq!(source_len, chars.len()); for start in 0..=source_len { for end in start..=source_len { for width in [0, 1, 2, 4, source_len.saturating_add(2)] { let left = start.saturating_sub(width); let right = end.saturating_add(width).min(source_len); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); normalize_chars_into( &chars[left..right], left, &mut expected, &mut expected_offsets, ); let observed = normalized_context_window( &normalized, &offsets, source_len, start, end, width, ); assert_eq!( observed, expected, "text={text:?}, start={start}, end={end}, width={width}" ); } } } } } #[test] fn borrowed_context_windows_preserve_phrase_boundaries() { let text = "prefederal -- Federal Reserve; GDP-growth; SECond SEC filing; SPY ETF"; let chars: Vec = text.chars().collect(); let mut normalized = Vec::new(); let mut offsets = Vec::new(); let (_, _, source_len) = normalize_text_into(text, &mut normalized, &mut offsets); let terms: [&[u8]; 7] = [ b"fed", b"federal reserve", b"gdp", b"growth", b"sec", b"spy", b"etf", ]; for start in 0..=source_len { for end in start..=source_len { for width in [0, 1, 8, source_len.saturating_add(1)] { let left = start.saturating_sub(width); let right = end.saturating_add(width).min(source_len); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); normalize_chars_into( &chars[left..right], left, &mut expected, &mut expected_offsets, ); let observed = normalized_context_window( &normalized, &offsets, source_len, start, end, width, ); for term in terms { assert_eq!( normalized_contains(observed, term), normalized_contains(&expected, term), "term={term:?}, start={start}, end={end}, width={width}" ); } } } } } #[test] fn custom_spy_context_keeps_reject_precedence() { let automaton = build_automaton(&["never matches".to_string()]).expect("valid normalized automaton"); let exact_specs = ExactSpecs::new(vec![ExactSpec { value: "SPY".chars().collect(), mode: ExactMode::CaseSensitive, kind: ALIAS, index: 0, }]) .expect("valid exact alias"); let aliases = vec![AliasSpec { canonical_group: 0, context_rule: Some(0), valid_from_epoch: 0, valid_to_epoch: None, family: FAMILY_SP500, entity_kind: 0, unresolved_tag: false, }]; let contexts = vec![ContextRule { window_chars: 128, accept_clauses: vec![ vec![b"etf".to_vec()], vec![b"fund".to_vec(), b"s&p 500".to_vec()], ], reject_terms: vec![b"spy plane".to_vec(), b"espionage".to_vec()], }]; let mut scratch = ScanScratch::default(); let scan = |text: &str, row_index: usize, scratch: &mut ScanScratch| { scan_one( &automaton, &[Vec::new()], &[], &exact_specs, &aliases, &[], &contexts, &[], text, 1, row_index, scratch, ) .expect("SPY remains an observed alias") }; let accepted = scan("SPY — ETF shares rose", 0, &mut scratch); assert_eq!(accepted.1, ARTICLE_ACCEPTED); assert_eq!(accepted.2, EXP_SUPPORTED); let rejected = scan("SPY ETF appeared beside a spy plane", 1, &mut scratch); assert_eq!(rejected.1, ARTICLE_REJECTED); assert_eq!(rejected.2, EXP_NOT_SUPPORTED); let missing = scan("SPY reported quarterly earnings", 2, &mut scratch); assert_eq!(missing.1, ARTICLE_REJECTED); assert_eq!(missing.2, EXP_NOT_SUPPORTED); } #[test] fn representative_fed_gdp_and_sec_contexts_keep_frozen_behavior() { let automaton = build_automaton(&["federal reserve".to_string()]).expect("valid normalized automaton"); let pattern_consumers = vec![vec![0]]; let consumers = vec![ConsumerSpec { kind: LEXICAL, a: 0, b: 0, c: 0, }]; let exact_specs = ExactSpecs::new(vec![ ExactSpec { value: "GDP".chars().collect(), mode: ExactMode::CaseSensitive, kind: LEXICAL, index: 1, }, ExactSpec { value: "SEC".chars().collect(), mode: ExactMode::CaseSensitive, kind: LEXICAL, index: 2, }, ]) .expect("valid exact lexical matchers"); let lexical = vec![ LexicalSpec { context_window_chars: 0, context_terms: Vec::new(), is_event: false, market_wide_authorized: true, unresolved_tag: false, has_event_family: false, }, LexicalSpec { context_window_chars: 64, context_terms: vec![b"growth".to_vec(), b"economy".to_vec()], is_event: false, market_wide_authorized: true, unresolved_tag: false, has_event_family: false, }, LexicalSpec { context_window_chars: 96, context_terms: vec![b"commission".to_vec(), b"filing".to_vec()], is_event: false, market_wide_authorized: false, unresolved_tag: false, has_event_family: false, }, ]; let mut scratch = ScanScratch::default(); let scan = |text: &str, row_index: usize, scratch: &mut ScanScratch| { scan_one( &automaton, &pattern_consumers, &consumers, &exact_specs, &[], &lexical, &[], &[], text, 1, row_index, scratch, ) }; let fed = scan("The Federal Reserve cut rates", 0, &mut scratch) .expect("Federal Reserve is relevant"); assert_eq!(fed.1, ARTICLE_ACCEPTED); assert_eq!(fed.5, EXP_SUPPORTED); let gdp = scan("Economic growth lifted GDP", 1, &mut scratch) .expect("contextual GDP is observed"); assert_eq!(gdp.1, ARTICLE_ACCEPTED); assert_eq!(gdp.5, EXP_SUPPORTED); let gdp_without_context = scan("The GDP widget was renamed", 2, &mut scratch) .expect("GDP remains an observation when context fails"); assert_eq!(gdp_without_context.1, ARTICLE_REJECTED); assert_eq!(gdp_without_context.5, EXP_NOT_SUPPORTED); let sec = scan("The SEC filing was published", 3, &mut scratch) .expect("contextual SEC is observed"); assert_eq!(sec.1, ARTICLE_ACCEPTED); assert_eq!(sec.5, EXP_NOT_SUPPORTED); assert!(scan("SECond place", 4, &mut scratch).is_none()); } #[test] fn reused_scan_scratch_does_not_leak_candidates_between_rows() { let automaton = build_automaton(&["alpha".to_string()]).expect("valid automaton"); let pattern_consumers = vec![vec![0]]; let consumers = vec![ConsumerSpec { kind: ALIAS, a: 0, b: 0, c: 0, }]; let exact_specs = ExactSpecs::new(Vec::new()).expect("empty exact specs are valid"); let aliases = vec![AliasSpec { canonical_group: 0, context_rule: None, valid_from_epoch: 0, valid_to_epoch: None, family: FAMILY_SP500, entity_kind: 0, unresolved_tag: false, }]; let mut scratch = ScanScratch::default(); let scan = |text: &str, row_index: usize, scratch: &mut ScanScratch| { scan_one( &automaton, &pattern_consumers, &consumers, &exact_specs, &aliases, &[], &[], &[], text, 1, row_index, scratch, ) }; let first = scan("Alpha rises", 0, &mut scratch).expect("first row matches"); assert_eq!(first.0, 0); assert_eq!(first.6, 1); assert!(scan("nothing relevant", 1, &mut scratch).is_none()); let third = scan("alpha again", 2, &mut scratch).expect("third row matches"); assert_eq!(third.0, 2); assert_eq!(third.6, 1); } #[test] fn conjunction_marks_resize_reset_and_survive_generation_wrap() { let mut marks = ConjunctionMarks::default(); marks.begin_row(130); for field in 0..4 { marks.mark(129, field); } assert_eq!(marks.touched, [129]); assert!(marks.is_complete(129)); marks.begin_row(130); assert!(marks.is_empty()); assert!(!marks.is_complete(129)); marks.generation = u32::MAX; marks.begin_row(130); assert_eq!(marks.generation, 1); assert!(marks.stamps.iter().all(|stamp| *stamp == 0)); assert!(!marks.is_complete(129)); } #[test] fn sorted_alias_candidates_preserve_longest_ties_and_frozen_counting() { let automaton = build_automaton(&["alpha".to_string(), "alpha beta".to_string()]) .expect("valid automaton"); let pattern_consumers = vec![vec![0], vec![1, 1, 2]]; let consumers = vec![ ConsumerSpec { kind: ALIAS, a: 0, b: 0, c: 0, }, ConsumerSpec { kind: ALIAS, a: 1, b: 0, c: 0, }, ConsumerSpec { kind: ALIAS, a: 2, b: 0, c: 0, }, ]; let aliases = vec![ AliasSpec { canonical_group: 0, context_rule: None, valid_from_epoch: 0, valid_to_epoch: None, family: FAMILY_SP500, entity_kind: 0, unresolved_tag: false, }, AliasSpec { canonical_group: 0, context_rule: None, valid_from_epoch: 0, valid_to_epoch: None, family: FAMILY_SP500, entity_kind: 0, unresolved_tag: false, }, // Retained observations are counted before temporal rejection. AliasSpec { canonical_group: 0, context_rule: None, valid_from_epoch: 10, valid_to_epoch: None, family: FAMILY_SP500, entity_kind: 0, unresolved_tag: false, }, ]; let exact_specs = ExactSpecs::new(Vec::new()).expect("empty exact specs are valid"); let mut scratch = ScanScratch::default(); let observed = scan_one( &automaton, &pattern_consumers, &consumers, &exact_specs, &aliases, &[], &[], &[], "alpha beta", 1, 0, &mut scratch, ) .expect("long aliases match"); assert_eq!(observed.1, ARTICLE_ACCEPTED); assert_eq!(observed.2, EXP_SUPPORTED); assert_eq!(observed.6, 2); } #[test] fn sorted_lexical_candidates_deduplicate_per_rule_and_span() { let automaton = build_automaton(&["signal".to_string()]).expect("valid automaton"); let pattern_consumers = vec![vec![0, 0]]; let consumers = vec![ConsumerSpec { kind: LEXICAL, a: 0, b: 0, c: 0, }]; let lexical = vec![LexicalSpec { context_window_chars: 0, context_terms: Vec::new(), is_event: true, market_wide_authorized: false, unresolved_tag: false, has_event_family: true, }]; let exact_specs = ExactSpecs::new(Vec::new()).expect("empty exact specs are valid"); let mut scratch = ScanScratch::default(); let observed = scan_one( &automaton, &pattern_consumers, &consumers, &exact_specs, &[], &lexical, &[], &[], "signal and signal", 1, 0, &mut scratch, ) .expect("lexical events match"); assert_eq!(observed.1, ARTICLE_ACCEPTED); assert_eq!(observed.6, 2); } #[test] fn conjunction_masks_count_once_and_do_not_leak_between_rows() { let automaton = build_automaton(&[ "commission".to_string(), "adopted".to_string(), "rule".to_string(), "market".to_string(), ]) .expect("valid automaton"); let pattern_consumers = vec![vec![0, 0], vec![1, 1], vec![2], vec![3]]; let consumers = vec![ ConsumerSpec { kind: CONJUNCTION, a: 0, b: 0, c: 0, }, ConsumerSpec { kind: CONJUNCTION, a: 0, b: 1, c: 0, }, ConsumerSpec { kind: CONJUNCTION, a: 0, b: 2, c: 0, }, ConsumerSpec { kind: CONJUNCTION, a: 0, b: 3, c: 0, }, ]; let exact_specs = ExactSpecs::new(vec![ExactSpec { value: "SEC".chars().collect(), mode: ExactMode::CaseSensitive, kind: CONJUNCTION, index: 0, }]) .expect("valid exact actor"); let conjunctions = vec![ConjunctionSpec { actor_phrase_count: 1, action_phrase_count: 1, object_phrase_count: 1, scope_phrase_count: 1, }]; let mut scratch = ScanScratch::default(); let scan = |text: &str, row_index: usize, scratch: &mut ScanScratch| { scan_one( &automaton, &pattern_consumers, &consumers, &exact_specs, &[], &[], &[], &conjunctions, text, 1, row_index, scratch, ) }; let first = scan( "SEC commission adopted adopted rule market", 0, &mut scratch, ) .expect("complete conjunction"); assert_eq!(first.6, 1); assert_eq!(first.5, EXP_SUPPORTED); assert!(scan("SEC adopted rule", 1, &mut scratch).is_none()); let third = scan("commission adopted rule market", 2, &mut scratch) .expect("later complete conjunction"); assert_eq!(third.6, 1); } // Block fixtures. Existing normalization_reference_checker is also // changed by the proposal to run each current scalar fixture in both modes. #[cfg(target_arch = "x86_64")] #[test] fn mixed_avx2_prefix_stops_at_original_unicode_boundaries() { println!("Phase31 block runtime AVX2 available: {}", mixed_ascii_avx2_available()); if !mixed_ascii_avx2_available() { return; // The portable/forced-scalar fixtures still run everywhere. } let pattern = b"A \t-`B/\0\x1c\x7f.,:;()[]{}"; for prefix_len in 0..=96 { for character in ['é', 'fi', 'İ', 'ı', 'ſ', 'K', '界', '🦀', '\u{fdfa}'] { let prefix: Vec = pattern.iter().copied().cycle().take(prefix_len).collect(); let mut text = String::from_utf8(prefix.clone()).unwrap(); text.push(character); text.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"); for carry_space in [false, true] { // Nonzero base proves offsets are source positions, not // local block positions. A previous expansion is retained. let mut expected = b"fi".to_vec(); let mut expected_offsets = vec![5, 5]; let mut expected_pending = carry_space; for (index, byte) in prefix.iter().copied().enumerate() { normalize_source_char(byte as char, 9 + index, &mut expected, &mut expected_offsets, &mut expected_pending); } let mut observed = b"fi".to_vec(); let mut observed_offsets = vec![5, 5]; let mut observed_pending = carry_space; // SAFETY: runtime AVX2 check above; helper bounds every load. let consumed = unsafe { normalize_mixed_ascii_prefix_avx2_into( text.as_bytes(), 9, &mut observed, &mut observed_offsets, &mut observed_pending, ) }; assert_eq!(consumed, prefix_len, "prefix={prefix_len} char={character:?}"); assert_eq!(observed, expected); assert_eq!(observed_offsets, expected_offsets); assert_eq!(observed_pending, expected_pending); } } } // Every ASCII byte in every SIMD lane, including whitespace and // separators inside blocks. This does not rely on whitespace-free data. for lane in 0..96 { for byte in 0u8..=0x7f { let mut prefix = vec![b'x'; 96]; prefix[lane] = byte; let mut text = String::from_utf8(prefix.clone()).unwrap(); text.push_str("fiABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); let mut expected_pending = false; for (index, source_byte) in prefix.iter().copied().enumerate() { normalize_source_char(source_byte as char, 7 + index, &mut expected, &mut expected_offsets, &mut expected_pending); } let mut observed = Vec::new(); let mut observed_offsets = Vec::new(); let mut observed_pending = false; let consumed = unsafe { normalize_mixed_ascii_prefix_avx2_into( text.as_bytes(), 7, &mut observed, &mut observed_offsets, &mut observed_pending, ) }; assert_eq!(consumed, 96); assert_eq!(observed, expected, "byte={byte} lane={lane}"); assert_eq!(observed_offsets, expected_offsets, "byte={byte} lane={lane}"); assert_eq!(observed_pending, expected_pending, "byte={byte} lane={lane}"); } } // No padded/overreading load on short tails. Complete mixed fixtures // exercise the caller's scalar processing of these untouched bytes. for length in [0, 1, 15, 16, 31, 32, 33, 63, 64, 65] { let input = vec![b'A'; length]; let mut normalized = Vec::new(); let mut offsets = Vec::new(); let mut pending_space = false; let consumed = unsafe { normalize_mixed_ascii_prefix_avx2_into( &input, 7, &mut normalized, &mut offsets, &mut pending_space, ) }; assert_eq!(consumed, (length / 32) * 32); assert_eq!(normalized, vec![b'a'; consumed]); assert_eq!(offsets, (7..7 + consumed).collect::>()); } } #[test] fn mixed_block_modes_collapse_whole_blocks_across_expansions() { let mut check = normalization_reference_checker(); for block_len in [32, 64] { // Every lane belongs to the collapse mask, including across two // consecutive blocks. Include spaces, controls and punctuation. let collapse: String = b" \t\n\r\x0b\x0c\x1c\x1d\x1e\x1f-.,:;()[]{}/" .iter().copied().cycle().take(block_len).map(char::from).collect(); for text in [ // No previous output: all-one masks must not create leading space. format!("{collapse}B\u{00a0}"), // Carry space through one/two masks after a Unicode expansion. format!("fi{collapse}B"), // Trailing masks must remain pending, never emitted at end-of-row. format!("fi{collapse}"), // A scalar Unicode space starts the same pending state before // the first vector block; repeated blocks must not duplicate it. format!("fi\u{00a0}{collapse}B"), ] { check(&text); } for allow_avx2 in [false, true] { let mut normalized = Vec::new(); let mut offsets = Vec::new(); let text = format!("fi{collapse}B"); let flags = normalize_text_into_with_mixed_avx2( &text, &mut normalized, &mut offsets, allow_avx2, ); assert_eq!(flags, (false, false, block_len + 2)); assert_eq!(normalized, b"fi b"); assert_eq!(offsets, [0, 0, block_len + 1, block_len + 1]); let text = format!("fi{collapse}"); normalize_text_into_with_mixed_avx2( &text, &mut normalized, &mut offsets, allow_avx2, ); assert_eq!(normalized, b"fi"); assert_eq!(offsets, [0, 0]); } } } #[test] fn mixed_block_modes_keep_context_windows_across_vector_edges() { let texts = [ format!("{}fi --\tİ{}\u{00a0}B", "A".repeat(31), "Z".repeat(33)), format!("\u{00a0}{}—{}\u{fdfa} ", "B`/ ".repeat(16), "C".repeat(31)), format!("{}Kıſ{}🦀{}", "A".repeat(32), "D".repeat(32), "E".repeat(33)), ]; for text in texts { let chars: Vec = text.chars().collect(); for allow_avx2 in [false, true] { let mut normalized = Vec::new(); let mut offsets = Vec::new(); let flags = normalize_text_into_with_mixed_avx2( &text, &mut normalized, &mut offsets, allow_avx2, ); assert_eq!(flags.2, chars.len()); for start in [0, 1, 15, 16, 31, 32, 33, 63, 64, chars.len()] { let start = start.min(chars.len()); for span in [0, 1, 7] { let end = (start + span).min(chars.len()); for width in [0, 1, 2, 31, 32, 33, 65] { let left = start.saturating_sub(width); let right = end.saturating_add(width).min(chars.len()); let mut expected = Vec::new(); let mut expected_offsets = Vec::new(); normalize_chars_into(&chars[left..right], left, &mut expected, &mut expected_offsets); assert_eq!( normalized_context_window(&normalized, &offsets, chars.len(), start, end, width), expected.as_slice(), "allow_avx2={allow_avx2} start={start} end={end} width={width}", ); } } } } } } }