matcher_rs 0.12.3

A high-performance matcher designed to solve LOGICAL and TEXT VARIATIONS problems in word matching, implemented in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
//! Thread-local scan state for [`super::SimpleMatcher`].
//!
//! All mutable state needed during a scan is kept in a single [`SimpleMatchState`] instance
//! per thread, accessed through the `#[thread_local]` static [`SIMPLE_MATCH_STATE`]. This
//! avoids per-call allocation: the backing storage grows monotonically and is reused across
//! matchers and across calls.
//!
//! # Generation-based state reset
//!
//! Instead of zeroing every [`WordState`] between calls, a monotonic `generation` counter
//! is bumped in [`SimpleMatchState::prepare`]. A rule's state is "live" only when its
//! stored generation stamp matches the current generation. Stale entries are effectively
//! invisible, giving O(1) amortized reset cost.
//!
//! When `generation` wraps to `u32::MAX`, all stamps are reset to 0 and the counter
//! restarts at 1. This happens at most once every ~4 billion calls per thread.

use std::cell::UnsafeCell;

use tinyvec::TinyVec;

use super::rule::RuleHot;

/// Per-rule mutable state reused across scans.
///
/// Each rule has one `WordState` slot in [`SimpleMatchState::word_states`], indexed by
/// `rule_idx`. Fields use generation stamps rather than boolean flags so the entire
/// vector can be "reset" by incrementing the global generation counter.
///
/// # Layout
///
/// The three `*_generation` fields track whether the rule has been touched / satisfied /
/// vetoed in the current scan. `satisfied_mask` and `remaining_and` are only meaningful
/// when `matrix_generation == current_generation` (i.e., the rule has been initialized
/// for this scan).
#[derive(Default, Clone, Copy)]
pub(super) struct WordState {
    /// Generation in which the rule's matrix/bitmask state was initialized.
    ///
    /// Set to the current generation on first touch. If it does not match the current
    /// generation, the rest of this struct's fields are stale.
    pub(super) matrix_generation: u32,
    /// Generation in which all positive (AND) requirements became satisfied.
    ///
    /// Set to the current generation when `remaining_and` reaches zero or on a
    /// [`PatternKind::Simple`](super::rule::PatternKind::Simple) hit. A rule is
    /// considered "satisfied" when `positive_generation == current_generation` and
    /// `not_generation != current_generation`.
    pub(super) positive_generation: u32,
    /// Generation in which a NOT segment vetoed the rule.
    ///
    /// Once set, the rule cannot fire regardless of how many AND segments match.
    pub(super) not_generation: u32,
    /// Bitset fast path for tracking which AND segments have been satisfied.
    ///
    /// Bit `i` is set when segment `i` has been observed at least once. Only used
    /// when [`RuleHot::use_matrix`](super::rule::RuleHot::use_matrix) is `false`
    /// and the rule has more than one AND segment.
    pub(super) satisfied_mask: u64,
    /// Remaining AND segments still needed before the rule can fire.
    ///
    /// Initialized to [`RuleHot::and_count`](super::rule::RuleHot::and_count) and
    /// decremented as segments are satisfied. The rule becomes positive when this
    /// reaches zero.
    pub(super) remaining_and: u16,
}

/// Thread-local state reused by every [`super::SimpleMatcher`] call on one thread.
///
/// Backing storage grows monotonically to accommodate the largest rule set seen on this
/// thread. Between calls, only [`prepare`](Self::prepare) is needed — it bumps the
/// generation and clears the touched-indices list without touching the bulk arrays.
///
/// # Matrix layout
///
/// For rules with [`RuleHot::use_matrix`](super::rule::RuleHot::use_matrix) = `true`,
/// `matrix[rule_idx]` is a flat 2-D array of shape `[num_segments × num_variants]`
/// stored in row-major order. Each cell starts at the segment's required count and is
/// decremented (AND) or incremented (NOT) on each hit. `matrix_status[rule_idx]` is a
/// parallel 1-D array of per-segment completion flags.
pub(super) struct SimpleMatchState {
    /// Per-rule state slots indexed by rule id.
    pub(super) word_states: Vec<WordState>,
    /// Per-variant counter matrix for complex rules (one `TinyVec` per rule index).
    ///
    /// `matrix[rule_idx][segment * num_variants + variant_idx]` holds the remaining
    /// count for that segment in that variant. Initialized lazily on first touch.
    pub(super) matrix: Vec<TinyVec<[i32; 16]>>,
    /// Per-segment completion flags for complex rules (one `TinyVec` per rule index).
    ///
    /// `matrix_status[rule_idx][segment]` is 0 if the segment is still pending, 1 if
    /// it has been satisfied (AND) or triggered (NOT).
    pub(super) matrix_status: Vec<TinyVec<[u8; 16]>>,
    /// Rule indices touched during the current scan generation.
    ///
    /// Cleared at the start of each scan in [`prepare`](Self::prepare). Used by
    /// [`RuleSet::collect_matches`](super::rule::RuleSet::collect_matches) and
    /// [`RuleSet::has_match`](super::rule::RuleSet::has_match) to iterate only over
    /// rules that received at least one pattern hit.
    pub(super) touched_indices: Vec<usize>,
    /// Monotonic generation id used to avoid clearing full state between calls.
    generation: u32,
}

/// Thread-local reusable scan state shared by all matchers on the current thread.
///
/// # Safety
///
/// The `UnsafeCell` is safe to use here because:
///
/// 1. `#[thread_local]` guarantees that each thread has its own instance — no cross-thread
///    sharing occurs.
/// 2. The scan functions that access this static (`is_match_inner`, `process_simple`,
///    `process_preprocessed_into`) are not re-entrant: they obtain a `&mut` reference via
///    `SIMPLE_MATCH_STATE.get()` at the top of the call and hold it for the entire
///    duration. No callback or nested call re-enters the same path.
///
/// This pattern avoids the overhead of `RefCell` on the scan hot path.
#[thread_local]
pub(super) static SIMPLE_MATCH_STATE: UnsafeCell<SimpleMatchState> =
    UnsafeCell::new(SimpleMatchState::new());

/// Scan metadata passed through the hot match-processing path.
///
/// One `ScanContext` is constructed per text variant and threaded through
/// [`RuleSet::process_entry`](super::rule::RuleSet::process_entry) for every hit in
/// that variant. Kept `Copy` to avoid reference overhead in tight loops.
#[derive(Clone, Copy)]
pub(super) struct ScanContext {
    /// Index of the current transformed text variant.
    ///
    /// Used as the column index into [`SimpleMatchState::matrix`] for matrix-mode rules.
    pub(super) text_index: usize,
    /// Bitmask of compact process-type indices that contributed to this variant.
    ///
    /// Bit `i` is set if the variant was produced by (or is reachable from) the process
    /// type whose compact index is `i`. Checked against
    /// [`PatternEntry::pt_index`](super::rule::PatternEntry::pt_index) to filter hits
    /// from irrelevant variants.
    pub(super) process_type_mask: u64,
    /// Total number of transformed variants participating in this scan.
    ///
    /// Determines the number of columns in the matrix for matrix-mode rules.
    pub(super) num_variants: usize,
    /// Whether the caller may stop on the first satisfied rule.
    ///
    /// `true` for `is_match` calls; `false` for `process` calls that must collect all
    /// matching rules.
    pub(super) exit_early: bool,
    /// Whether to use the bytewise engine for this text variant.
    ///
    /// `true` when the multi-byte density of the variant is below the per-plan
    /// charwise density threshold (see [`ScanPlan::charwise_density_threshold`](super::engine::ScanPlan::charwise_density_threshold)). Passed through to
    /// [`ScanPlan::for_each_match_value`](super::engine::ScanPlan::for_each_match_value)
    /// to select the bytewise or charwise automaton.
    pub(super) use_bytewise: bool,
}

/// Lifecycle helpers for the thread-local scan state.
impl SimpleMatchState {
    /// Creates an empty reusable state container with generation 0.
    ///
    /// All backing vectors start empty and grow on the first call to [`prepare`](Self::prepare).
    pub(super) const fn new() -> Self {
        Self {
            word_states: Vec::new(),
            matrix: Vec::new(),
            matrix_status: Vec::new(),
            touched_indices: Vec::new(),
            generation: 0,
        }
    }

    /// Advances the generation and grows backing storage for at least `size` rules.
    ///
    /// Must be called exactly once at the start of every scan before any state is read.
    /// On `u32::MAX` overflow, all generation stamps are bulk-reset to 0 and the
    /// counter restarts at 1.
    pub(super) fn prepare(&mut self, size: usize) {
        if self.generation == u32::MAX {
            for state in self.word_states.iter_mut() {
                state.matrix_generation = 0;
                state.positive_generation = 0;
                state.not_generation = 0;
            }
            self.generation = 1;
        } else {
            self.generation += 1;
        }

        if self.word_states.len() < size {
            self.word_states.resize(size, WordState::default());
            self.matrix.resize(size, TinyVec::new());
            self.matrix_status.resize(size, TinyVec::new());
        }

        self.touched_indices.clear();
    }

    /// Returns the current scan generation id.
    #[inline(always)]
    pub(super) fn generation(&self) -> u32 {
        self.generation
    }

    /// Returns the rules touched during the current generation.
    #[inline(always)]
    pub(super) fn touched_indices(&self) -> &[usize] {
        &self.touched_indices
    }

    /// Returns whether `rule_idx` is satisfied in the current generation.
    ///
    /// A rule is satisfied when all its AND segments have been observed
    /// (`positive_generation == generation`) and no NOT segment has vetoed it
    /// (`not_generation != generation`).
    ///
    /// # Safety
    ///
    /// Uses `get_unchecked` on `self.word_states`. Guarded by a preceding `debug_assert!`.
    ///
    /// # Panics
    ///
    /// Debug-asserts that `rule_idx < self.word_states.len()`.
    #[inline(always)]
    pub(super) fn rule_is_satisfied(&self, rule_idx: usize) -> bool {
        debug_assert!(rule_idx < self.word_states.len());
        // SAFETY: `rule_idx` is in bounds — guarded by the debug_assert above.
        let word_state = unsafe { self.word_states.get_unchecked(rule_idx) };
        word_state.positive_generation == self.generation
            && word_state.not_generation != self.generation
    }

    /// Marks a simple rule as positive for the current generation.
    ///
    /// Returns `true` only when this is the first positive hit for the rule in the current
    /// generation — used by the all-simple fast path to deduplicate results.
    ///
    /// If the rule has not been touched at all in this generation, it is also added to
    /// `touched_indices`.
    ///
    /// # Safety
    ///
    /// Uses `get_unchecked_mut` on `self.word_states`. Guarded by a preceding `debug_assert!`.
    ///
    /// # Panics
    ///
    /// Debug-asserts that `rule_idx < self.word_states.len()`.
    #[inline(always)]
    pub(super) fn mark_positive(&mut self, rule_idx: usize) -> bool {
        let generation = self.generation;
        debug_assert!(rule_idx < self.word_states.len());
        // SAFETY: `rule_idx` is in bounds — guarded by the debug_assert above.
        let word_state = unsafe { self.word_states.get_unchecked_mut(rule_idx) };
        if word_state.positive_generation == generation {
            return false;
        }
        if word_state.matrix_generation != generation {
            word_state.matrix_generation = generation;
            self.touched_indices.push(rule_idx);
        }
        word_state.positive_generation = generation;
        true
    }

    /// Initializes one complex rule the first time it is touched in a generation.
    ///
    /// Sets up the generation stamp, resets the remaining-AND counter and bitmask, adds
    /// the rule to `touched_indices`, and — for matrix-mode rules — initializes the
    /// per-variant counter matrix via [`init_matrix`].
    ///
    /// If the rule has zero AND segments (pure NOT rule), it is immediately marked
    /// positive since there is nothing to satisfy.
    ///
    /// # Safety
    ///
    /// Uses `get_unchecked_mut` on `self.word_states`, `self.matrix`, and
    /// `self.matrix_status`. All accesses are for `rule_idx` which was bounds-checked
    /// by the caller.
    #[inline(always)]
    pub(super) fn init_rule(&mut self, rule: &RuleHot, rule_idx: usize, ctx: ScanContext) {
        let generation = self.generation;
        // SAFETY: `rule_idx` is in bounds — caller guarantees it via `RuleSet` indexing.
        let word_state = unsafe { self.word_states.get_unchecked_mut(rule_idx) };
        word_state.matrix_generation = generation;
        word_state.positive_generation = if rule.and_count == 0 { generation } else { 0 };
        word_state.remaining_and = rule.and_count as u16;
        word_state.satisfied_mask = 0;
        self.touched_indices.push(rule_idx);

        if rule.use_matrix {
            init_matrix(
                // SAFETY: `rule_idx` is in bounds — `matrix` is resized to match
                // `word_states` in `prepare`.
                unsafe { self.matrix.get_unchecked_mut(rule_idx) },
                // SAFETY: `matrix_status` is resized identically to `matrix` in `prepare`.
                unsafe { self.matrix_status.get_unchecked_mut(rule_idx) },
                &rule.segment_counts,
                ctx.num_variants,
            );
        }
    }
}

/// Initializes the per-variant counter matrix for a complex rule.
///
/// Allocates (or re-sizes) `flat_matrix` to `num_segments × num_variants` cells and
/// fills each row with the segment's required count from `segment_counts`. Resets
/// `flat_status` to all-zero (no segment satisfied yet).
///
/// Marked `#[cold]` because matrix-mode rules are rare — most rules use the bitmask
/// fast path.
#[cold]
#[inline(never)]
fn init_matrix(
    flat_matrix: &mut TinyVec<[i32; 16]>,
    flat_status: &mut TinyVec<[u8; 16]>,
    segment_counts: &[i32],
    num_variants: usize,
) {
    let num_splits = segment_counts.len();
    flat_matrix.clear();
    flat_matrix.resize(num_splits * num_variants, 0i32);
    flat_status.clear();
    flat_status.resize(num_splits, 0u8);

    for (split_idx, &count) in segment_counts.iter().enumerate() {
        let row_start = split_idx * num_variants;
        flat_matrix[row_start..row_start + num_variants].fill(count);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_ctx(num_variants: usize, exit_early: bool) -> ScanContext {
        ScanContext {
            text_index: 0,
            process_type_mask: u64::MAX,
            num_variants,
            exit_early,
            use_bytewise: true,
        }
    }

    #[test]
    fn test_prepare_grows_storage() {
        let mut state = SimpleMatchState::new();
        assert_eq!(state.generation(), 0);
        state.prepare(10);
        assert!(state.word_states.len() >= 10);
        assert!(state.matrix.len() >= 10);
        assert!(state.matrix_status.len() >= 10);
        assert_eq!(state.generation(), 1);
        assert!(state.touched_indices().is_empty());
    }

    #[test]
    fn test_prepare_generation_increments() {
        let mut state = SimpleMatchState::new();
        state.prepare(1);
        assert_eq!(state.generation(), 1);
        state.prepare(1);
        assert_eq!(state.generation(), 2);
        state.prepare(1);
        assert_eq!(state.generation(), 3);
    }

    #[test]
    fn test_prepare_generation_wraparound() {
        let mut state = SimpleMatchState::new();
        state.prepare(3);
        let current = state.generation();
        state.word_states[0].positive_generation = current;
        state.word_states[1].matrix_generation = current;
        state.word_states[2].not_generation = current;

        // Force generation to u32::MAX - 1 so next prepare hits MAX
        state.generation = u32::MAX - 1;
        state.prepare(3);
        assert_eq!(state.generation(), u32::MAX);

        // Next prepare should wraparound: reset all stamps to 0, generation = 1
        state.prepare(3);
        assert_eq!(state.generation(), 1);
        for ws in &state.word_states {
            assert_eq!(ws.matrix_generation, 0);
            assert_eq!(ws.positive_generation, 0);
            assert_eq!(ws.not_generation, 0);
        }
    }

    #[test]
    fn test_mark_positive_dedup() {
        let mut state = SimpleMatchState::new();
        state.prepare(2);

        assert!(state.mark_positive(0), "first mark should return true");
        assert!(!state.mark_positive(0), "second mark should return false");
        assert_eq!(state.touched_indices(), &[0]);
    }

    #[test]
    fn test_rule_is_satisfied() {
        let mut state = SimpleMatchState::new();
        state.prepare(1);
        let current = state.generation();

        assert!(!state.rule_is_satisfied(0));

        state.word_states[0].positive_generation = current;
        assert!(state.rule_is_satisfied(0));

        // NOT veto overrides positive
        state.word_states[0].not_generation = current;
        assert!(!state.rule_is_satisfied(0));
    }

    #[test]
    fn test_init_rule_matrix() {
        let mut state = SimpleMatchState::new();
        state.prepare(1);

        let rule = RuleHot {
            segment_counts: vec![1, 1, 0],
            and_count: 2,
            use_matrix: true,
        };
        let ctx = make_ctx(2, false);
        state.init_rule(&rule, 0, ctx);

        assert_eq!(state.word_states[0].matrix_generation, state.generation());
        assert_eq!(state.word_states[0].remaining_and, 2);
        assert_eq!(state.word_states[0].satisfied_mask, 0);
        assert_eq!(state.touched_indices(), &[0]);

        // Matrix should be 3 segments × 2 variants = 6 cells
        assert_eq!(state.matrix[0].len(), 6);
        // Row 0: [1, 1], Row 1: [1, 1], Row 2 (NOT): [0, 0]
        assert_eq!(&state.matrix[0][..], &[1, 1, 1, 1, 0, 0]);
        assert_eq!(state.matrix_status[0].len(), 3);
        assert!(state.matrix_status[0].iter().all(|&s| s == 0));
    }
}