Skip to main content

_diffctx/
interval.rs

1use std::sync::Arc;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::types::{Fragment, FragmentId};
6
7pub struct IntervalIndex {
8    by_path: FxHashMap<Arc<str>, Vec<(u32, u32)>>,
9    ids: FxHashSet<FragmentId>,
10}
11
12impl IntervalIndex {
13    pub fn new() -> Self {
14        Self {
15            by_path: FxHashMap::default(),
16            ids: FxHashSet::default(),
17        }
18    }
19
20    pub fn add(&mut self, frag: &Fragment) {
21        self.add_id(&frag.id);
22    }
23
24    pub fn add_id(&mut self, frag_id: &FragmentId) {
25        self.ids.insert(frag_id.clone());
26        let intervals = self.by_path.entry(frag_id.path.clone()).or_default();
27        let item = (frag_id.start_line, frag_id.end_line);
28        let pos = intervals.binary_search(&item).unwrap_or_else(|e| e);
29        intervals.insert(pos, item);
30    }
31
32    pub fn contains(&self, frag_id: &FragmentId) -> bool {
33        self.ids.contains(frag_id)
34    }
35
36    pub fn overlaps(&self, frag: &Fragment) -> bool {
37        let intervals = match self.by_path.get(&frag.id.path) {
38            Some(v) => v,
39            None => return false,
40        };
41        let upper = intervals.partition_point(|&(s, _)| s <= frag.end_line());
42        for i in 0..upper {
43            let (start, end) = intervals[i];
44            if start == frag.start_line() && end == frag.end_line() {
45                continue;
46            }
47            // Strict `>`: a fragment starting on the very last line of an
48            // already-selected fragment shares exactly one boundary line. We
49            // deliberately tolerate that one-line overlap rather than drop the
50            // candidate, because compact languages (Rust/Go/Scala one-liners,
51            // Lisp `}{` chains) routinely produce back-to-back fragments sharing
52            // that boundary line; rejecting them would silently discard the
53            // next fragment's unique content for the sake of one duplicated line.
54            //
55            // KNOWN ASYMMETRY, pinned by
56            // `overlaps_tolerates_a_shared_boundary_in_one_direction_only`.
57            // `partition_point` bounds the scan by `start <= candidate.end`
58            // (non-strict) while this comparison is strict, so the tolerance
59            // applies in one direction only: selected [1,10] vs candidate
60            // [10,20] is kept, but the mirrored selected [10,20] vs candidate
61            // [1,10] is dropped. Which side a fragment lands on depends on
62            // greedy visit order, not on relevance. Adding the mirrored strict
63            // bound (`start < frag.end_line() &&`) makes it symmetric and is
64            // Q-class: on the 2725-case corpus it moves 3 cases above threshold
65            // (javascript_059, rust_008, rust_027) and 3 below
66            // (frontend_010, r_lang_006, r_lang_009) — net zero, so it belongs
67            // to a calibration cycle boundary, not to an incidental change.
68            if end > frag.start_line() {
69                return true;
70            }
71        }
72        false
73    }
74
75    pub fn is_superset_of(&self, frag: &Fragment) -> bool {
76        let intervals = match self.by_path.get(&frag.id.path) {
77            Some(v) => v,
78            None => return false,
79        };
80        let upper = intervals.partition_point(|&(s, _)| s <= frag.start_line());
81        for i in 0..upper {
82            let (start, end) = intervals[i];
83            if start == frag.start_line() && end == frag.end_line() {
84                continue;
85            }
86            if start <= frag.start_line() && frag.end_line() <= end {
87                return true;
88            }
89        }
90        false
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::types::FragmentKind;
98
99    fn frag(path: &str, start: u32, end: u32) -> Fragment {
100        Fragment {
101            id: FragmentId::new(Arc::from(path), start, end),
102            kind: FragmentKind::Function,
103            content: Arc::from(""),
104            identifiers: FxHashSet::default(),
105            token_count: 1,
106            symbol_name: None,
107        }
108    }
109
110    fn index_with(spans: &[(u32, u32)]) -> IntervalIndex {
111        let mut idx = IntervalIndex::new();
112        for &(s, e) in spans {
113            idx.add(&frag("a.rs", s, e));
114        }
115        idx
116    }
117
118    #[test]
119    fn overlaps_is_true_for_genuine_intersections() {
120        let idx = index_with(&[(10, 20)]);
121        for &(s, e) in &[(15, 25), (11, 19), (9, 21), (5, 15), (10, 20 + 1)] {
122            assert!(
123                idx.overlaps(&frag("a.rs", s, e)),
124                "[{s},{e}] should intersect [10,20]"
125            );
126        }
127    }
128
129    /// Pins the known asymmetry documented on the comparison in `overlaps`.
130    /// The forward direction is the deliberate boundary tolerance; the mirrored
131    /// direction drops the candidate for the same geometry, so the verdict
132    /// depends on greedy visit order. Making it symmetric is Q-class (see the
133    /// comment for the measured corpus effect) — this test exists so the
134    /// current behaviour cannot change silently, in either direction.
135    #[test]
136    fn overlaps_tolerates_a_shared_boundary_in_one_direction_only() {
137        assert!(
138            !index_with(&[(1, 10)]).overlaps(&frag("a.rs", 10, 20)),
139            "candidate starting on the selected fragment's last line must be tolerated"
140        );
141        assert!(
142            index_with(&[(10, 20)]).overlaps(&frag("a.rs", 1, 10)),
143            "the mirrored case is currently reported as overlapping; if this now \
144             passes as tolerated, the symmetry fix landed — update the corpus baseline"
145        );
146    }
147
148    #[test]
149    fn overlaps_verdicts_are_pinned_across_the_boundary_matrix() {
150        // (selected, candidate) -> expected verdict. Encodes the asymmetry
151        // above rather than assuming symmetry, so any change to either
152        // comparison shows up here as a concrete diff.
153        let expected = [
154            ((1, 10), (10, 20), false),
155            ((10, 20), (1, 10), true),
156            ((10, 20), (20, 30), false),
157            ((20, 30), (10, 20), true),
158            ((10, 20), (11, 19), true),
159            ((11, 19), (10, 20), true),
160            ((10, 20), (9, 21), true),
161            ((9, 21), (10, 20), true),
162            // Another face of the same asymmetry: a one-line fragment at the
163            // selected span's start does not block it, but the reverse does.
164            ((10, 20), (10, 10), true),
165            ((10, 10), (10, 20), false),
166        ];
167        for (selected, candidate, want) in expected {
168            let got = index_with(&[selected]).overlaps(&frag("a.rs", candidate.0, candidate.1));
169            assert_eq!(
170                got, want,
171                "selected {selected:?} vs candidate {candidate:?}: got {got}, want {want}"
172            );
173        }
174    }
175
176    #[test]
177    fn overlaps_ignores_other_paths() {
178        let idx = index_with(&[(10, 20)]);
179        assert!(!idx.overlaps(&frag("b.rs", 15, 16)));
180    }
181
182    #[test]
183    fn identical_span_is_reported_by_contains_not_by_overlaps() {
184        // The exact-span `continue` means a duplicate is NOT an overlap, so
185        // callers must rely on `contains` to avoid charging the budget twice.
186        let idx = index_with(&[(10, 20)]);
187        let same = frag("a.rs", 10, 20);
188        assert!(!idx.overlaps(&same));
189        assert!(idx.contains(&same.id));
190    }
191
192    #[test]
193    fn is_superset_of_detects_enclosure_and_ignores_identical_spans() {
194        let idx = index_with(&[(10, 30)]);
195        assert!(idx.is_superset_of(&frag("a.rs", 15, 25)));
196        assert!(idx.is_superset_of(&frag("a.rs", 10, 25)));
197        assert!(!idx.is_superset_of(&frag("a.rs", 10, 30)));
198        assert!(!idx.is_superset_of(&frag("a.rs", 5, 25)));
199        assert!(!idx.is_superset_of(&frag("a.rs", 25, 35)));
200    }
201
202    #[test]
203    fn add_keeps_intervals_sorted_regardless_of_insertion_order() {
204        let forward = index_with(&[(1, 5), (10, 20), (30, 40)]);
205        let shuffled = index_with(&[(30, 40), (1, 5), (10, 20)]);
206        let path: Arc<str> = Arc::from("a.rs");
207        assert_eq!(forward.by_path[&path], shuffled.by_path[&path]);
208        assert!(forward.by_path[&path].windows(2).all(|w| w[0] <= w[1]));
209    }
210}