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<ByteRange>`, 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/// Operation-local region triple used by sort and dedup kernels.
49///
50/// Product boundaries use `vyre_foundation::match_result::ByteRange`; this type
51/// retains `pid` because the region ABI operates directly on packed pattern ids.
52///
53/// `pid`: pattern id; `start` / `end`: byte offsets, half-open
54/// `[start, end)`.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct RegionTriple {
57 /// Pattern id (which detector emitted this match).
58 pub pid: u32,
59 /// Inclusive start byte offset.
60 pub start: u32,
61 /// Exclusive end byte offset.
62 pub end: u32,
63}
64
65impl RegionTriple {
66 /// Construct a region triple. `end` must be `>= start`; equal
67 /// values represent a zero-width match (legal for some regex
68 /// constructs).
69 #[must_use]
70 pub const fn new(pid: u32, start: u32, end: u32) -> Self {
71 Self { pid, start, end }
72 }
73}
74
75impl Ord for RegionTriple {
76 fn cmp(&self, other: &Self) -> Ordering {
77 // Sort by (pid, start, end) so the dedup loop sees cluster
78 // members consecutively without a secondary group-by pass.
79 self.pid
80 .cmp(&other.pid)
81 .then(self.start.cmp(&other.start))
82 .then(self.end.cmp(&other.end))
83 }
84}
85
86impl PartialOrd for RegionTriple {
87 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
88 Some(self.cmp(other))
89 }
90}
91
92/// Reference CPU implementation: collapse same-pid overlapping spans.
93///
94/// Sort happens inline (`sort_unstable`); the input may arrive in any
95/// order. Pre-sorted callers should still see linear behavior since
96/// `sort_unstable` is `O(n log n)` worst case, `O(n)` on already-
97/// sorted input.
98#[must_use]
99#[cfg(any(test, feature = "cpu-parity"))]
100pub fn dedup_regions_cpu(input: Vec<RegionTriple>) -> Vec<RegionTriple> {
101 let mut owned = input;
102 dedup_regions_inplace(&mut owned);
103 owned
104}
105
106/// CPU reference for [`region_sort_program`] - stable lexicographic
107/// sort of `(pid, start, end)` triples by composite key.
108///
109/// `dedup_regions_inplace` already sorts internally, so callers that
110/// only want dedup don't need this helper. It exists for parity tests
111/// against the GPU sort and for pipelines that need the sorted-but-
112/// not-yet-deduped view (e.g. when stream_compact runs separately).
113#[cfg(any(test, feature = "cpu-parity"))]
114pub fn sort_regions_cpu(input: &mut [RegionTriple]) {
115 input.sort();
116}
117
118/// Sort and merge overlapping regions in place.
119///
120/// Regions are ordered by `(pid, start, end)`. Adjacent entries with the same
121/// pattern id and overlapping or touching byte spans are coalesced into a
122/// single [`RegionTriple`]. The vector is truncated to the deduplicated length.
123#[cfg(any(test, feature = "cpu-parity"))]
124pub fn dedup_regions_inplace(input: &mut Vec<RegionTriple>) {
125 if input.is_empty() {
126 return;
127 }
128 input.sort_unstable();
129
130 // Two-cursor compaction: `write` indexes the next slot to populate,
131 // `read` walks the (sorted) input. Each merge folds the read entry
132 // into `input[write - 1]`; each non-merge advances `write`.
133 let mut write = 1usize;
134 for read in 1..input.len() {
135 let next = input[read];
136 let last = input[write - 1];
137 let same_pid = next.pid == last.pid;
138 let overlap_or_touch = next.start <= last.end;
139 if same_pid && overlap_or_touch {
140 if next.end > last.end {
141 input[write - 1].end = next.end;
142 }
143 } else {
144 input[write] = next;
145 write += 1;
146 }
147 }
148 input.truncate(write);
149}
150
151/// Reference CPU companion to [`cap_regions_per_pattern_flag_program`].
152///
153/// Returns one survivor flag per input slot (`1` = keep, `0` = drop): the first
154/// `k` matches of each pattern id **in array order** survive, the rest are
155/// dropped. This is the exact contract the GPU flag kernel encodes, a single
156/// running-count pass, the independent oracle the parity test checks the kernel
157/// against. Keys only on `pid`; on `(pid, start, end)`-sorted input the survivors
158/// are the `k` earliest-start matches per pattern.
159#[cfg(any(test, feature = "cpu-parity"))]
160#[must_use]
161pub fn cap_regions_per_pattern_survivors_cpu(pids: &[u32], k: u32) -> Vec<u32> {
162 use std::collections::HashMap;
163 let mut seen: HashMap<u32, u32> = HashMap::new();
164 pids.iter()
165 .map(|&pid| {
166 let count = seen.entry(pid).or_insert(0);
167 let survivor = u32::from(*count < k);
168 *count += 1;
169 survivor
170 })
171 .collect()
172}
173
174/// Reference CPU companion to [`compact_first_per_region_pattern_flag_program`].
175///
176/// Returns one survivor flag per input slot (`1` = keep, `0` = drop): the first
177/// occurrence of each `(region, pid)` pair **in array order** survives, every
178/// later occurrence of that pair is dropped. This is the exact contract the GPU
179/// flag kernel encodes, a single first-occurrence pass over a `HashSet` of seen
180/// pairs (the independent oracle the parity test checks the kernel against).
181/// Stream-compacting on these flags leaves exactly one positioned representative
182/// per `(region, pid)`, the positioned form of the presence-by-region bitmap.
183#[cfg(any(test, feature = "cpu-parity"))]
184#[must_use]
185pub fn compact_first_per_region_pattern_survivors_cpu(regions: &[u32], pids: &[u32]) -> Vec<u32> {
186 use std::collections::HashSet;
187 assert_eq!(
188 regions.len(),
189 pids.len(),
190 "regions and pids columns must be parallel"
191 );
192 let mut seen: HashSet<(u32, u32)> = HashSet::new();
193 regions
194 .iter()
195 .zip(pids.iter())
196 .map(|(®ion, &pid)| u32::from(seen.insert((region, pid))))
197 .collect()
198}