vyre_primitives/range.rs
1//! Byte-range primitive - a domain-neutral `(tag, start, end)` triple.
2//!
3//! CRITIQUE_VISION_ALIGNMENT_2026-04-23 V1 (forward-compatible half):
4//! `vyre-foundation` currently ships a matching-domain-flavoured
5//! `Match { pattern_id, start, end }` struct as its Tier-1 scan-result
6//! type. That name (`Match`) and the field (`pattern_id`) pre-decide
7//! that byte ranges are *matches* from a *pattern* - a Tier-3
8//! matching-dialect concept. A crypto-decoder dialect returning decode
9//! spans, an AST-span dialect, a taint-source locator, a regex
10//! capture-group emitter - none of them have "patterns". They all
11//! want `(tag, start, end)`.
12//!
13//! This module introduces the neutral name **without breaking the
14//! foundation API**: `ByteRange` is a brand-new Tier 2.5 type that
15//! lives under `vyre_primitives::range`. New dialects adopt
16//! `ByteRange` directly. Legacy callers keep using `vyre::Match` as
17//! long as they want; zero-cost `From` conversions bridge the two so
18//! a consumer can accept either.
19//!
20//! The bridge below is the migration surface: new code uses
21//! `ByteRange`, while legacy `vyre::Match` callers interoperate
22//! through zero-cost conversions.
23
24/// A tagged, half-open byte range `[start, end)`.
25///
26/// `tag` is a producer-chosen 32-bit identifier - a matching dialect
27/// can pass a `pattern_id`, a decoder can pass an encoding ID, an
28/// AST-span emitter can pass a node kind, a taint-source locator can
29/// pass a source index. The producer decides what it means; the type
30/// carries no domain assumption.
31///
32/// The struct is deliberately `#[repr(C)]` so FFI and backend
33/// marshalling share one layout, and `#[non_exhaustive]` so future
34/// fields (capture groups, confidence, …) can be added without
35/// breaking the API.
36///
37/// # Examples
38///
39/// ```
40/// use vyre_primitives::range::ByteRange;
41///
42/// let r = ByteRange::new(7, 10, 20);
43/// assert_eq!(r.tag, 7);
44/// assert_eq!(r.start, 10);
45/// assert_eq!(r.end, 20);
46/// assert_eq!(r.len(), 10);
47/// ```
48#[repr(C)]
49#[non_exhaustive]
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub struct ByteRange {
52 /// Producer-chosen 32-bit identifier. Not interpreted by this crate.
53 pub tag: u32,
54 /// Inclusive byte start offset.
55 pub start: u32,
56 /// Exclusive byte end offset.
57 pub end: u32,
58}
59
60impl ByteRange {
61 /// Construct a range. `end` MUST be `>= start`; the assertion
62 /// catches reversed ranges in both debug and release so producers
63 /// hit the bug at the call site instead of downstream.
64 ///
65 /// AUDIT_2026-04-24 F-RANGE-01: promoted `debug_assert!` to
66 /// `assert!` so release builds can't silently accept a reversed
67 /// range (which used to cascade into `len()` returning `0` and
68 /// every range-containment predicate answering the wrong way).
69 #[must_use]
70 pub const fn new(tag: u32, start: u32, end: u32) -> Self {
71 assert!(
72 end >= start,
73 "ByteRange end must be greater than or equal to start"
74 );
75 Self { tag, start, end }
76 }
77
78 /// Length of the range in bytes.
79 ///
80 /// AUDIT_2026-04-24 F-RANGE-02: uses plain subtraction so any
81 /// reversed range triggers a panic in release. Prior
82 /// `saturating_sub` hid producer bugs by returning `0` for
83 /// ill-formed ranges; `new()`'s release-time assertion now
84 /// prevents that state from reaching here in the first place,
85 /// and the plain op gives a second fail-loud line of defense if
86 /// a caller forged a `ByteRange` through the public fields.
87 #[must_use]
88 pub const fn len(&self) -> u32 {
89 self.end - self.start
90 }
91
92 /// True when the range has zero length.
93 #[must_use]
94 pub const fn is_empty(&self) -> bool {
95 self.end == self.start
96 }
97
98 /// True when `self` contains `other` (both start and end).
99 #[must_use]
100 pub const fn contains(&self, other: &ByteRange) -> bool {
101 self.start <= other.start && other.end <= self.end
102 }
103
104 /// True when `self` ends at or before `other` starts (disjoint,
105 /// `self` first). Mirrors the `Before` predicate surfaced by
106 /// scanner dialects.
107 #[must_use]
108 pub const fn ends_before(&self, other: &ByteRange) -> bool {
109 self.end <= other.start
110 }
111}
112
113// Bridges to/from `vyre_foundation::match_result::Match` live
114// behind any Tier-2.5 domain feature that already pulls
115// vyre-foundation. The default build stays dep-free; enabling any
116// primitive domain flag enables the bridges too.
117#[cfg(feature = "vyre-foundation")]
118mod match_bridge {
119 use super::ByteRange;
120
121 impl From<vyre_foundation::match_result::Match> for ByteRange {
122 fn from(m: vyre_foundation::match_result::Match) -> Self {
123 ByteRange::new(m.pattern_id, m.start, m.end)
124 }
125 }
126
127 impl From<ByteRange> for vyre_foundation::match_result::Match {
128 fn from(r: ByteRange) -> Self {
129 vyre_foundation::match_result::Match::new(r.tag, r.start, r.end)
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn new_roundtrip() {
140 let r = ByteRange::new(42, 100, 200);
141 assert_eq!(r.tag, 42);
142 assert_eq!(r.start, 100);
143 assert_eq!(r.end, 200);
144 assert_eq!(r.len(), 100);
145 assert!(!r.is_empty());
146 }
147
148 #[test]
149 fn empty_is_zero_length() {
150 let r = ByteRange::new(1, 5, 5);
151 assert!(r.is_empty());
152 assert_eq!(r.len(), 0);
153 }
154
155 #[test]
156 fn contains_inclusive_bounds() {
157 let outer = ByteRange::new(0, 0, 100);
158 let inner = ByteRange::new(0, 10, 90);
159 assert!(outer.contains(&inner));
160 assert!(!inner.contains(&outer));
161 // A range contains itself.
162 assert!(outer.contains(&outer));
163 }
164
165 #[test]
166 fn ends_before_requires_disjoint() {
167 let a = ByteRange::new(0, 0, 10);
168 let b = ByteRange::new(0, 10, 20);
169 let c = ByteRange::new(0, 5, 15);
170 assert!(a.ends_before(&b));
171 assert!(!a.ends_before(&c));
172 }
173
174 #[cfg(feature = "vyre-foundation")]
175 #[test]
176 fn bridge_from_match_preserves_fields() {
177 let m = vyre_foundation::match_result::Match::new(7, 11, 22);
178 let r: ByteRange = m.into();
179 assert_eq!(r.tag, 7);
180 assert_eq!(r.start, 11);
181 assert_eq!(r.end, 22);
182 }
183
184 #[cfg(feature = "vyre-foundation")]
185 #[test]
186 fn bridge_back_to_match_preserves_fields() {
187 let r = ByteRange::new(9, 13, 33);
188 let m: vyre_foundation::match_result::Match = r.into();
189 assert_eq!(m.pattern_id, 9);
190 assert_eq!(m.start, 13);
191 assert_eq!(m.end, 33);
192 }
193
194 #[test]
195 fn layout_is_repr_c_u32x3() {
196 // The layout stability is load-bearing for backend marshalling
197 // and FFI. Pinning here means future field additions cannot
198 // quietly break the wire shape without this test flipping.
199 assert_eq!(std::mem::size_of::<ByteRange>(), 12);
200 assert_eq!(std::mem::align_of::<ByteRange>(), 4);
201 }
202}