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}