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 Self {
113 pattern_id,
114 start,
115 end,
116 }
117 }
118}
119
120impl From<Match> for ByteRange {
121 fn from(value: Match) -> Self {
122 ByteRange::new(value.pattern_id, value.start, value.end)
123 }
124}
125
126impl From<ByteRange> for Match {
127 fn from(value: ByteRange) -> Self {
128 Match::new(value.tag, value.start, value.end)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::collections::HashSet;
136
137 #[test]
138 fn construction() {
139 let m = Match::new(1, 10, 20);
140 assert_eq!(m.pattern_id, 1);
141 assert_eq!(m.start, 10);
142 assert_eq!(m.end, 20);
143 }
144
145 #[test]
146 fn ordering() {
147 let a = Match::new(0, 5, 10);
148 let b = Match::new(0, 15, 20);
149 let c = Match::new(1, 0, 5);
150 let mut v = [c, a, b];
151 v.sort();
152 assert_eq!(v[0].start, 5);
153 assert_eq!(v[1].start, 15);
154 assert_eq!(v[2].pattern_id, 1);
155 }
156
157 #[test]
158 fn clone_and_eq() {
159 let a = Match::new(1, 0, 100);
160 let b = a;
161 assert_eq!(a, b);
162 }
163
164 #[test]
165 fn hash_consistency() {
166 let mut set = HashSet::new();
167 let m = Match::new(1, 0, 10);
168 set.insert(m);
169 assert!(set.contains(&Match::new(1, 0, 10)));
170 assert!(!set.contains(&Match::new(2, 0, 10)));
171 }
172
173 #[test]
174 fn byte_range_bridge_preserves_fields() {
175 let range = ByteRange::new(7, 11, 22);
176 let matched: Match = range.into();
177 assert_eq!(matched.pattern_id, 7);
178 assert_eq!(matched.start, 11);
179 assert_eq!(matched.end, 22);
180 let roundtrip: ByteRange = matched.into();
181 assert_eq!(roundtrip, range);
182 }
183
184 #[test]
185 #[should_panic(expected = "ByteRange::new requires end >= start")]
186 fn byte_range_rejects_reversed_ranges() {
187 let _ = ByteRange::new(1, 10, 9);
188 }
189}