Skip to main content

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}