vyre_primitives/matching/region.rs
1//! Span-region dedup primitive.
2//!
3//! Every multimatch consumer (`vyre-libs::matching` engines, scanner consumer,
4//! external analyzer) ends up doing the same operation after the GPU dispatch
5//! returns: take the raw `Vec<Match>`, collapse adjacent overlapping
6//! or duplicate spans into a representative, return the deduped set.
7//! Each consumer wrote it differently - some by `(detector_id,
8//! credential)` HashMap, some by `(start, end)` pair sort, some by ad-
9//! hoc loop. The lego-block fix is one primitive every consumer calls.
10//!
11//! # Algorithm
12//!
13//! Given a slice of `(pid, start, end)` triples sorted by `(pid, start, end)`,
14//! emit one representative per maximal cluster of triples that
15//! overlap or touch (`start[i] <= end[max_end_so_far]`) AND have the same
16//! `pid`. This collapses both:
17//!
18//! - `(pid=0, 5, 10)` and `(pid=0, 6, 11)` → `(pid=0, 5, 11)`
19//! (overlapping, same pattern - extend span).
20//! - `(pid=0, 5, 10)` and `(pid=0, 5, 10)` → one entry
21//! (exact dup).
22//!
23//! Distinct `pid`s never merge - two patterns matching the same
24//! region produce two output spans (cross-pattern dedup is a
25//! different operation; consumers that want it apply a second pass).
26//!
27//! # CPU + GPU
28//!
29//! - `dedup_regions_cpu` is the reference implementation: pure data,
30//! no IR, no backend. CPU-side consumers and parity tests use it.
31//! - `region_sort_program` and `dedup_regions_cluster_program` emit
32//! GPU-resident sorted spans, survivor flags, and merged cluster ends
33//! so parser/scanner pipelines can compact deduped triples without a
34//! host readback between stages.
35//!
36//! Both share a single golden test fixture set so any divergence is
37//! caught at conform time.
38
39use std::cmp::Ordering;
40
41pub use super::region_programs::{
42 cap_regions_per_pattern_flag_program, compact_first_per_region_pattern_flag_program,
43 dedup_regions_cluster_program, dedup_regions_flag_program, region_dedup_dispatch_grid,
44 region_sort_program, CAP_REGIONS_PER_PATTERN_OP_ID, COMPACT_FIRST_PER_REGION_PATTERN_OP_ID,
45 DEDUP_REGIONS_CLUSTER_OP_ID, DEDUP_REGIONS_FLAG_OP_ID, REGION_DEDUP_WORKGROUP_SIZE,
46};
47
48/// One match as exposed by `vyre_foundation::match_result::Match` -
49/// duplicated here as a plain triple so this primitive doesn't depend
50/// on foundation. Consumers convert at the boundary.
51///
52/// `pid`: pattern id; `start` / `end`: byte offsets, half-open
53/// `[start, end)`.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct RegionTriple {
56 /// Pattern id (which detector emitted this match).
57 pub pid: u32,
58 /// Inclusive start byte offset.
59 pub start: u32,
60 /// Exclusive end byte offset.
61 pub end: u32,
62}
63
64impl RegionTriple {
65 /// Construct a region triple. `end` must be `>= start`; equal
66 /// values represent a zero-width match (legal for some regex
67 /// constructs).
68 #[must_use]
69 pub const fn new(pid: u32, start: u32, end: u32) -> Self {
70 Self { pid, start, end }
71 }
72}
73
74impl Ord for RegionTriple {
75 fn cmp(&self, other: &Self) -> Ordering {
76 // Sort by (pid, start, end) so the dedup loop sees cluster
77 // members consecutively without a secondary group-by pass.
78 self.pid
79 .cmp(&other.pid)
80 .then(self.start.cmp(&other.start))
81 .then(self.end.cmp(&other.end))
82 }
83}
84
85impl PartialOrd for RegionTriple {
86 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
87 Some(self.cmp(other))
88 }
89}
90
91/// Reference CPU implementation: collapse same-pid overlapping spans.
92///
93/// Sort happens inline (`sort_unstable`); the input may arrive in any
94/// order. Pre-sorted callers should still see linear behavior since
95/// `sort_unstable` is `O(n log n)` worst case, `O(n)` on already-
96/// sorted input.
97#[must_use]
98#[cfg(any(test, feature = "cpu-parity"))]
99pub fn dedup_regions_cpu(input: Vec<RegionTriple>) -> Vec<RegionTriple> {
100 let mut owned = input;
101 dedup_regions_inplace(&mut owned);
102 owned
103}
104
105/// CPU reference for [`region_sort_program`] - stable lexicographic
106/// sort of `(pid, start, end)` triples by composite key.
107///
108/// `dedup_regions_inplace` already sorts internally, so callers that
109/// only want dedup don't need this helper. It exists for parity tests
110/// against the GPU sort and for pipelines that need the sorted-but-
111/// not-yet-deduped view (e.g. when stream_compact runs separately).
112#[cfg(any(test, feature = "cpu-parity"))]
113pub fn sort_regions_cpu(input: &mut [RegionTriple]) {
114 input.sort();
115}
116
117/// Sort and merge overlapping regions in place.
118///
119/// Regions are ordered by `(pid, start, end)`. Adjacent entries with the same
120/// pattern id and overlapping or touching byte spans are coalesced into a
121/// single [`RegionTriple`]. The vector is truncated to the deduplicated length.
122#[cfg(any(test, feature = "cpu-parity"))]
123pub fn dedup_regions_inplace(input: &mut Vec<RegionTriple>) {
124 if input.is_empty() {
125 return;
126 }
127 input.sort_unstable();
128
129 // Two-cursor compaction: `write` indexes the next slot to populate,
130 // `read` walks the (sorted) input. Each merge folds the read entry
131 // into `input[write - 1]`; each non-merge advances `write`.
132 let mut write = 1usize;
133 for read in 1..input.len() {
134 let next = input[read];
135 let last = input[write - 1];
136 let same_pid = next.pid == last.pid;
137 let overlap_or_touch = next.start <= last.end;
138 if same_pid && overlap_or_touch {
139 if next.end > last.end {
140 input[write - 1].end = next.end;
141 }
142 } else {
143 input[write] = next;
144 write += 1;
145 }
146 }
147 input.truncate(write);
148}
149
150/// Reference CPU companion to [`cap_regions_per_pattern_flag_program`].
151///
152/// Returns one survivor flag per input slot (`1` = keep, `0` = drop): the first
153/// `k` matches of each pattern id **in array order** survive, the rest are
154/// dropped. This is the exact contract the GPU flag kernel encodes, a single
155/// running-count pass, the independent oracle the parity test checks the kernel
156/// against. Keys only on `pid`; on `(pid, start, end)`-sorted input the survivors
157/// are the `k` earliest-start matches per pattern.
158#[cfg(any(test, feature = "cpu-parity"))]
159#[must_use]
160pub fn cap_regions_per_pattern_survivors_cpu(pids: &[u32], k: u32) -> Vec<u32> {
161 use std::collections::HashMap;
162 let mut seen: HashMap<u32, u32> = HashMap::new();
163 pids.iter()
164 .map(|&pid| {
165 let count = seen.entry(pid).or_insert(0);
166 let survivor = u32::from(*count < k);
167 *count += 1;
168 survivor
169 })
170 .collect()
171}
172
173/// Reference CPU companion to [`compact_first_per_region_pattern_flag_program`].
174///
175/// Returns one survivor flag per input slot (`1` = keep, `0` = drop): the first
176/// occurrence of each `(region, pid)` pair **in array order** survives, every
177/// later occurrence of that pair is dropped. This is the exact contract the GPU
178/// flag kernel encodes, a single first-occurrence pass over a `HashSet` of seen
179/// pairs (the independent oracle the parity test checks the kernel against).
180/// Stream-compacting on these flags leaves exactly one positioned representative
181/// per `(region, pid)`, the positioned form of the presence-by-region bitmap.
182#[cfg(any(test, feature = "cpu-parity"))]
183#[must_use]
184pub fn compact_first_per_region_pattern_survivors_cpu(regions: &[u32], pids: &[u32]) -> Vec<u32> {
185 use std::collections::HashSet;
186 assert_eq!(
187 regions.len(),
188 pids.len(),
189 "regions and pids columns must be parallel"
190 );
191 let mut seen: HashSet<(u32, u32)> = HashSet::new();
192 regions
193 .iter()
194 .zip(pids.iter())
195 .map(|(®ion, &pid)| u32::from(seen.insert((region, pid))))
196 .collect()
197}