infiniloom_engine/
newtypes.rs

1//! Type-safe wrappers for primitive types
2//!
3//! This module provides newtype wrappers that prevent accidentally mixing
4//! different kinds of values (e.g., token counts vs line counts).
5//!
6//! # Example
7//!
8//! ```rust
9//! use infiniloom_engine::newtypes::{TokenCount, LineNumber, ByteOffset};
10//!
11//! let tokens = TokenCount::new(1000);
12//! let line = LineNumber::new(42);
13//!
14//! // These are different types and can't be accidentally mixed
15//! // tokens + line; // This would be a compile error
16//! ```
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use std::ops::{Add, AddAssign, Sub, SubAssign};
21
22/// A count of tokens (for LLM context budgeting)
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
24#[repr(transparent)]
25pub struct TokenCount(u32);
26
27impl TokenCount {
28    /// Create a new token count
29    #[inline]
30    pub const fn new(count: u32) -> Self {
31        Self(count)
32    }
33
34    /// Create a zero token count
35    #[inline]
36    pub const fn zero() -> Self {
37        Self(0)
38    }
39
40    /// Get the inner value
41    #[inline]
42    pub const fn get(self) -> u32 {
43        self.0
44    }
45
46    /// Check if this is zero
47    #[inline]
48    pub const fn is_zero(self) -> bool {
49        self.0 == 0
50    }
51
52    /// Saturating subtraction
53    #[inline]
54    pub const fn saturating_sub(self, rhs: Self) -> Self {
55        Self(self.0.saturating_sub(rhs.0))
56    }
57
58    /// Saturating addition
59    #[inline]
60    pub const fn saturating_add(self, rhs: Self) -> Self {
61        Self(self.0.saturating_add(rhs.0))
62    }
63
64    /// Calculate percentage of another token count
65    #[inline]
66    pub fn percentage_of(self, total: Self) -> f32 {
67        if total.0 == 0 {
68            0.0
69        } else {
70            (self.0 as f32 / total.0 as f32) * 100.0
71        }
72    }
73}
74
75impl Add for TokenCount {
76    type Output = Self;
77
78    #[inline]
79    fn add(self, rhs: Self) -> Self::Output {
80        Self(self.0 + rhs.0)
81    }
82}
83
84impl AddAssign for TokenCount {
85    #[inline]
86    fn add_assign(&mut self, rhs: Self) {
87        self.0 += rhs.0;
88    }
89}
90
91impl Sub for TokenCount {
92    type Output = Self;
93
94    #[inline]
95    fn sub(self, rhs: Self) -> Self::Output {
96        Self(self.0 - rhs.0)
97    }
98}
99
100impl SubAssign for TokenCount {
101    #[inline]
102    fn sub_assign(&mut self, rhs: Self) {
103        self.0 -= rhs.0;
104    }
105}
106
107impl From<u32> for TokenCount {
108    #[inline]
109    fn from(value: u32) -> Self {
110        Self(value)
111    }
112}
113
114impl From<TokenCount> for u32 {
115    #[inline]
116    fn from(value: TokenCount) -> Self {
117        value.0
118    }
119}
120
121impl fmt::Display for TokenCount {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{} tokens", self.0)
124    }
125}
126
127impl std::iter::Sum for TokenCount {
128    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
129        iter.fold(Self::zero(), |acc, x| acc + x)
130    }
131}
132
133/// A 1-indexed line number in source code
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
135#[repr(transparent)]
136pub struct LineNumber(u32);
137
138impl LineNumber {
139    /// Create a new line number (1-indexed)
140    #[inline]
141    pub const fn new(line: u32) -> Self {
142        Self(line)
143    }
144
145    /// Get the first line (line 1)
146    #[inline]
147    pub const fn first() -> Self {
148        Self(1)
149    }
150
151    /// Get the inner value
152    #[inline]
153    pub const fn get(self) -> u32 {
154        self.0
155    }
156
157    /// Check if this is a valid line number (> 0)
158    #[inline]
159    pub const fn is_valid(self) -> bool {
160        self.0 > 0
161    }
162
163    /// Convert to 0-indexed offset
164    #[inline]
165    pub const fn to_zero_indexed(self) -> u32 {
166        self.0.saturating_sub(1)
167    }
168
169    /// Create from 0-indexed offset
170    #[inline]
171    pub const fn from_zero_indexed(offset: u32) -> Self {
172        Self(offset + 1)
173    }
174
175    /// Calculate line count between this and another line (inclusive)
176    #[inline]
177    pub const fn lines_to(self, end: Self) -> u32 {
178        if end.0 >= self.0 {
179            end.0 - self.0 + 1
180        } else {
181            1
182        }
183    }
184}
185
186impl From<u32> for LineNumber {
187    #[inline]
188    fn from(value: u32) -> Self {
189        Self(value)
190    }
191}
192
193impl From<LineNumber> for u32 {
194    #[inline]
195    fn from(value: LineNumber) -> Self {
196        value.0
197    }
198}
199
200impl fmt::Display for LineNumber {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(f, "L{}", self.0)
203    }
204}
205
206/// A byte offset in a file or string
207#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
208#[repr(transparent)]
209pub struct ByteOffset(usize);
210
211impl ByteOffset {
212    /// Create a new byte offset
213    #[inline]
214    pub const fn new(offset: usize) -> Self {
215        Self(offset)
216    }
217
218    /// Create a zero offset
219    #[inline]
220    pub const fn zero() -> Self {
221        Self(0)
222    }
223
224    /// Get the inner value
225    #[inline]
226    pub const fn get(self) -> usize {
227        self.0
228    }
229}
230
231impl From<usize> for ByteOffset {
232    #[inline]
233    fn from(value: usize) -> Self {
234        Self(value)
235    }
236}
237
238impl From<ByteOffset> for usize {
239    #[inline]
240    fn from(value: ByteOffset) -> Self {
241        value.0
242    }
243}
244
245impl fmt::Display for ByteOffset {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "@{}", self.0)
248    }
249}
250
251/// A unique identifier for a symbol in the index
252#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
253#[repr(transparent)]
254pub struct SymbolId(u32);
255
256impl SymbolId {
257    /// Create a new symbol ID
258    #[inline]
259    pub const fn new(id: u32) -> Self {
260        Self(id)
261    }
262
263    /// Create an invalid/unknown symbol ID
264    #[inline]
265    pub const fn unknown() -> Self {
266        Self(0)
267    }
268
269    /// Get the inner value
270    #[inline]
271    pub const fn get(self) -> u32 {
272        self.0
273    }
274
275    /// Check if this is a valid symbol ID
276    #[inline]
277    pub const fn is_valid(self) -> bool {
278        self.0 > 0
279    }
280}
281
282impl From<u32> for SymbolId {
283    #[inline]
284    fn from(value: u32) -> Self {
285        Self(value)
286    }
287}
288
289impl From<SymbolId> for u32 {
290    #[inline]
291    fn from(value: SymbolId) -> Self {
292        value.0
293    }
294}
295
296impl fmt::Display for SymbolId {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        write!(f, "#{}", self.0)
299    }
300}
301
302/// File size in bytes
303#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
304#[repr(transparent)]
305pub struct FileSize(u64);
306
307impl FileSize {
308    /// Create a new file size
309    #[inline]
310    pub const fn new(bytes: u64) -> Self {
311        Self(bytes)
312    }
313
314    /// Create a zero size
315    #[inline]
316    pub const fn zero() -> Self {
317        Self(0)
318    }
319
320    /// Get the inner value in bytes
321    #[inline]
322    pub const fn bytes(self) -> u64 {
323        self.0
324    }
325
326    /// Get size in kilobytes
327    #[inline]
328    pub const fn kilobytes(self) -> u64 {
329        self.0 / 1024
330    }
331
332    /// Get size in megabytes
333    #[inline]
334    pub const fn megabytes(self) -> u64 {
335        self.0 / (1024 * 1024)
336    }
337
338    /// Check if this exceeds a limit
339    #[inline]
340    pub const fn exceeds(self, limit: Self) -> bool {
341        self.0 > limit.0
342    }
343}
344
345impl From<u64> for FileSize {
346    #[inline]
347    fn from(value: u64) -> Self {
348        Self(value)
349    }
350}
351
352impl From<FileSize> for u64 {
353    #[inline]
354    fn from(value: FileSize) -> Self {
355        value.0
356    }
357}
358
359impl fmt::Display for FileSize {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        if self.0 >= 1024 * 1024 {
362            write!(f, "{:.1} MB", self.0 as f64 / (1024.0 * 1024.0))
363        } else if self.0 >= 1024 {
364            write!(f, "{:.1} KB", self.0 as f64 / 1024.0)
365        } else {
366            write!(f, "{} bytes", self.0)
367        }
368    }
369}
370
371/// Importance score (0.0 to 1.0)
372#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
373#[repr(transparent)]
374pub struct ImportanceScore(f32);
375
376impl ImportanceScore {
377    /// Create a new importance score, clamping to valid range
378    #[inline]
379    pub fn new(score: f32) -> Self {
380        Self(score.clamp(0.0, 1.0))
381    }
382
383    /// Create a zero importance score
384    #[inline]
385    pub const fn zero() -> Self {
386        Self(0.0)
387    }
388
389    /// Create maximum importance score
390    #[inline]
391    pub const fn max() -> Self {
392        Self(1.0)
393    }
394
395    /// Create default importance score
396    #[inline]
397    pub const fn default_score() -> Self {
398        Self(0.5)
399    }
400
401    /// Get the inner value
402    #[inline]
403    pub const fn get(self) -> f32 {
404        self.0
405    }
406
407    /// Check if this is considered high importance (> 0.7)
408    #[inline]
409    pub fn is_high(self) -> bool {
410        self.0 > 0.7
411    }
412
413    /// Check if this is considered low importance (< 0.3)
414    #[inline]
415    pub fn is_low(self) -> bool {
416        self.0 < 0.3
417    }
418}
419
420impl From<f32> for ImportanceScore {
421    #[inline]
422    fn from(value: f32) -> Self {
423        Self::new(value)
424    }
425}
426
427impl From<ImportanceScore> for f32 {
428    #[inline]
429    fn from(value: ImportanceScore) -> Self {
430        value.0
431    }
432}
433
434impl fmt::Display for ImportanceScore {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        write!(f, "{:.2}", self.0)
437    }
438}
439
440impl Eq for ImportanceScore {}
441
442#[allow(clippy::derive_ord_xor_partial_ord)]
443impl Ord for ImportanceScore {
444    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
445        self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
446    }
447}
448
449impl std::hash::Hash for ImportanceScore {
450    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
451        self.0.to_bits().hash(state);
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    // TokenCount tests
460    #[test]
461    fn test_token_count_operations() {
462        let a = TokenCount::new(100);
463        let b = TokenCount::new(50);
464
465        assert_eq!((a + b).get(), 150);
466        assert_eq!((a - b).get(), 50);
467        assert_eq!(a.saturating_sub(TokenCount::new(200)).get(), 0);
468    }
469
470    #[test]
471    fn test_token_count_percentage() {
472        let part = TokenCount::new(25);
473        let total = TokenCount::new(100);
474
475        assert!((part.percentage_of(total) - 25.0).abs() < 0.01);
476        assert_eq!(part.percentage_of(TokenCount::zero()), 0.0);
477    }
478
479    #[test]
480    fn test_token_count_sum() {
481        let counts = vec![TokenCount::new(10), TokenCount::new(20), TokenCount::new(30)];
482        let sum: TokenCount = counts.into_iter().sum();
483        assert_eq!(sum.get(), 60);
484    }
485
486    // LineNumber tests
487    #[test]
488    fn test_line_number_indexing() {
489        let line = LineNumber::new(10);
490
491        assert_eq!(line.to_zero_indexed(), 9);
492        assert_eq!(LineNumber::from_zero_indexed(9).get(), 10);
493    }
494
495    #[test]
496    fn test_line_number_range() {
497        let start = LineNumber::new(5);
498        let end = LineNumber::new(10);
499
500        assert_eq!(start.lines_to(end), 6);
501        assert_eq!(end.lines_to(start), 1); // Invalid range returns 1
502    }
503
504    // ByteOffset tests
505    #[test]
506    fn test_byte_offset() {
507        let offset = ByteOffset::new(1024);
508        assert_eq!(offset.get(), 1024);
509        assert_eq!(ByteOffset::zero().get(), 0);
510    }
511
512    // SymbolId tests
513    #[test]
514    fn test_symbol_id_validity() {
515        assert!(!SymbolId::unknown().is_valid());
516        assert!(SymbolId::new(1).is_valid());
517        assert!(!SymbolId::new(0).is_valid());
518    }
519
520    // FileSize tests
521    #[test]
522    fn test_file_size_conversions() {
523        let size = FileSize::new(1024 * 1024 + 512 * 1024); // 1.5 MB
524
525        assert_eq!(size.kilobytes(), 1536);
526        assert_eq!(size.megabytes(), 1);
527    }
528
529    #[test]
530    fn test_file_size_display() {
531        assert_eq!(FileSize::new(500).to_string(), "500 bytes");
532        assert_eq!(FileSize::new(2048).to_string(), "2.0 KB");
533        assert_eq!(FileSize::new(1024 * 1024).to_string(), "1.0 MB");
534    }
535
536    // ImportanceScore tests
537    #[test]
538    fn test_importance_score_clamping() {
539        assert_eq!(ImportanceScore::new(-0.5).get(), 0.0);
540        assert_eq!(ImportanceScore::new(1.5).get(), 1.0);
541        assert_eq!(ImportanceScore::new(0.5).get(), 0.5);
542    }
543
544    #[test]
545    fn test_importance_score_classification() {
546        assert!(ImportanceScore::new(0.8).is_high());
547        assert!(!ImportanceScore::new(0.5).is_high());
548        assert!(ImportanceScore::new(0.2).is_low());
549        assert!(!ImportanceScore::new(0.5).is_low());
550    }
551
552    // Display tests
553    #[test]
554    fn test_display_formatting() {
555        assert_eq!(TokenCount::new(100).to_string(), "100 tokens");
556        assert_eq!(LineNumber::new(42).to_string(), "L42");
557        assert_eq!(ByteOffset::new(1000).to_string(), "@1000");
558        assert_eq!(SymbolId::new(5).to_string(), "#5");
559    }
560}