Skip to main content

vyre_foundation/runtime/
match_result.rs

1//! Native scan match result  -  **legacy scan-domain shim.**
2//!
3//! CRITIQUE_VISION_ALIGNMENT_2026-04-23 V1: this type was the Tier-1
4//! return shape for every byte-range scan in vyre. Its field name
5//! (`pattern_id`) pre-decided that every byte range is a "match"
6//! from a "pattern"  -  a matching-dialect concept that shouldn't
7//! live in foundation. A crypto decoder, an AST-span emitter, or a
8//! capture-group producer would either adopt matching vocabulary
9//! awkwardly or ship a parallel type.
10//!
11//! The canonical neutral name is `ByteRange`. `Match` remains here as
12//! a backward-compat scan-domain shape. Bridges between the two types
13//! are zero-cost (`repr(C)` u32×3 on both sides).
14//!
15//! The full migration removes `Match` entirely; we keep it for one
16//! release so dependent crates don't hard-break.
17
18/// A tagged, half-open byte range `[start, end)`.
19///
20/// `tag` is a producer-chosen 32-bit identifier. A matching dialect can pass a
21/// pattern id, a decoder can pass an encoding id, and a source-span producer
22/// can pass a node kind. Foundation does not interpret the field.
23#[repr(C)]
24#[non_exhaustive]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct ByteRange {
27    /// Producer-chosen 32-bit identifier.
28    pub tag: u32,
29    /// Inclusive byte start offset.
30    pub start: u32,
31    /// Exclusive byte end offset.
32    pub end: u32,
33}
34
35impl ByteRange {
36    /// Construct a range. Reversed ranges fail loudly because accepting them
37    /// corrupts every downstream range predicate.
38    #[must_use]
39    pub const fn new(tag: u32, start: u32, end: u32) -> Self {
40        assert!(
41            end >= start,
42            "ByteRange::new requires end >= start. Fix: pass half-open byte ranges as [start, end)."
43        );
44        Self { tag, start, end }
45    }
46
47    /// Length of the range in bytes.
48    #[must_use]
49    pub const fn len(&self) -> u32 {
50        self.end - self.start
51    }
52
53    /// True when the range has zero length.
54    #[must_use]
55    pub const fn is_empty(&self) -> bool {
56        self.end == self.start
57    }
58
59    /// True when `self` contains `other`.
60    #[must_use]
61    pub const fn contains(&self, other: &ByteRange) -> bool {
62        self.start <= other.start && other.end <= self.end
63    }
64
65    /// True when `self` ends at or before `other` starts.
66    #[must_use]
67    pub const fn ends_before(&self, other: &ByteRange) -> bool {
68        self.end <= other.start
69    }
70}
71
72/// A byte-range match emitted by vyre scanning engines.
73///
74/// **Deprecated:** callers should migrate to
75/// [`ByteRange`]. The two types share layout and the `From` bridges are
76/// zero-cost.
77///
78/// Background: `pattern_id` is a matching-dialect concept. The
79/// neutral name on the new type is `tag`; the producer decides what
80/// it means (pattern id, encoding id, AST kind, source index, …).
81/// CRITIQUE_VISION_ALIGNMENT_2026-04-23 V1.
82#[non_exhaustive]
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct Match {
85    /// Stable pattern identifier that produced the match.
86    pub pattern_id: u32,
87    /// Inclusive byte start offset.
88    pub start: u32,
89    /// Exclusive byte end offset.
90    pub end: u32,
91}
92
93impl Match {
94    /// Construct a match from its pattern id and byte range.
95    ///
96    /// This constructor is a const fn so that engines can emit match
97    /// literals at compile time. The byte range is half-open `[start, end)`
98    /// to match Rust slicing conventions.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use vyre::Match;
104    ///
105    /// let m = Match::new(1, 10, 20);
106    /// assert_eq!(m.pattern_id, 1);
107    /// assert_eq!(m.start, 10);
108    /// assert_eq!(m.end, 20);
109    /// ```
110    #[must_use]
111    pub const fn new(pattern_id: u32, start: u32, end: u32) -> Self {
112        // Same half-open `[start, end)` invariant as the sibling `ByteRange`,
113        // so the zero-cost `From<Match> for ByteRange` bridge cannot panic on a
114        // value `Match` itself permitted. Reversed ranges fail loudly at
115        // construction, not later at conversion.
116        assert!(
117            end >= start,
118            "Match::new requires end >= start. Fix: pass half-open byte ranges as [start, end)."
119        );
120        Self {
121            pattern_id,
122            start,
123            end,
124        }
125    }
126}
127
128impl From<Match> for ByteRange {
129    fn from(value: Match) -> Self {
130        ByteRange::new(value.pattern_id, value.start, value.end)
131    }
132}
133
134impl From<ByteRange> for Match {
135    fn from(value: ByteRange) -> Self {
136        Match::new(value.tag, value.start, value.end)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use std::collections::HashSet;
144
145    #[test]
146    fn construction() {
147        let m = Match::new(1, 10, 20);
148        assert_eq!(m.pattern_id, 1);
149        assert_eq!(m.start, 10);
150        assert_eq!(m.end, 20);
151    }
152
153    #[test]
154    fn ordering() {
155        let a = Match::new(0, 5, 10);
156        let b = Match::new(0, 15, 20);
157        let c = Match::new(1, 0, 5);
158        let mut v = [c, a, b];
159        v.sort();
160        assert_eq!(v[0].start, 5);
161        assert_eq!(v[1].start, 15);
162        assert_eq!(v[2].pattern_id, 1);
163    }
164
165    #[test]
166    fn clone_and_eq() {
167        let a = Match::new(1, 0, 100);
168        let b = a;
169        assert_eq!(a, b);
170    }
171
172    #[test]
173    fn hash_consistency() {
174        let mut set = HashSet::new();
175        let m = Match::new(1, 0, 10);
176        set.insert(m);
177        assert!(set.contains(&Match::new(1, 0, 10)));
178        assert!(!set.contains(&Match::new(2, 0, 10)));
179    }
180
181    #[test]
182    fn byte_range_bridge_preserves_fields() {
183        let range = ByteRange::new(7, 11, 22);
184        let matched: Match = range.into();
185        assert_eq!(matched.pattern_id, 7);
186        assert_eq!(matched.start, 11);
187        assert_eq!(matched.end, 22);
188        let roundtrip: ByteRange = matched.into();
189        assert_eq!(roundtrip, range);
190    }
191
192    #[test]
193    #[should_panic(expected = "ByteRange::new requires end >= start")]
194    fn byte_range_rejects_reversed_ranges() {
195        let _ = ByteRange::new(1, 10, 9);
196    }
197
198    #[test]
199    #[should_panic(expected = "Match::new requires end >= start")]
200    fn match_rejects_reversed_ranges_at_construction() {
201        let _ = Match::new(1, 10, 9);
202    }
203
204    #[test]
205    fn match_to_byte_range_bridge_preserves_fields() {
206        let matched = Match::new(9, 4, 17);
207        let range: ByteRange = matched.into();
208        assert_eq!(range.tag, 9);
209        assert_eq!(range.start, 4);
210        assert_eq!(range.end, 17);
211        assert_eq!(range.len(), 13);
212    }
213}