Skip to main content

rez_next_version/
parser.rs

1//! High-performance version parsing utilities with zero-copy state machine
2
3use super::Version;
4use ahash::AHashMap;
5use once_cell::sync::Lazy;
6use rez_next_common::RezCoreError;
7use smallvec::SmallVec;
8use std::sync::RwLock;
9
10/// String interning pool for reducing memory allocations
11static STRING_INTERN_POOL: Lazy<RwLock<AHashMap<String, &'static str>>> =
12    Lazy::new(|| RwLock::new(AHashMap::new()));
13
14/// Token types for state machine parsing
15#[derive(Debug, Clone, PartialEq)]
16pub enum TokenType {
17    Numeric(u64),
18    Alphanumeric(String),
19    Separator(char),
20}
21
22/// Parser state for state machine
23#[derive(Debug, Clone, Copy, PartialEq)]
24enum ParseState {
25    Start,
26    InToken,
27    InSeparator,
28}
29
30/// High-performance version parser with state machine and zero-copy optimization
31pub struct StateMachineParser {
32    /// Enable string interning for memory optimization
33    use_interning: bool,
34    /// Maximum number of tokens allowed
35    max_tokens: usize,
36    /// Maximum number of numeric tokens allowed
37    max_numeric_tokens: usize,
38}
39
40impl StateMachineParser {
41    /// Create a new high-performance parser
42    #[must_use]
43    pub fn new() -> Self {
44        Self {
45            use_interning: true,
46            max_tokens: 10,
47            max_numeric_tokens: 5,
48        }
49    }
50
51    /// Create parser with custom configuration
52    #[must_use]
53    pub fn with_config(use_interning: bool, max_tokens: usize, max_numeric_tokens: usize) -> Self {
54        Self {
55            use_interning,
56            max_tokens,
57            max_numeric_tokens,
58        }
59    }
60
61    /// Intern a string to reduce memory allocations
62    fn intern_string(&self, s: String) -> String {
63        if !self.use_interning || s.len() > 64 {
64            return s;
65        }
66
67        // Try to get from pool first
68        {
69            let pool = STRING_INTERN_POOL.read().unwrap();
70            if let Some(&interned) = pool.get(&s) {
71                return interned.to_string();
72            }
73        }
74
75        // Add to pool if not found
76        {
77            let mut pool = STRING_INTERN_POOL.write().unwrap();
78            // Double-check after acquiring write lock
79            if let Some(&interned) = pool.get(&s) {
80                return interned.to_string();
81            }
82
83            // Limit pool size to prevent memory leaks
84            if pool.len() < 10000 {
85                let leaked: &'static str = Box::leak(s.clone().into_boxed_str());
86                pool.insert(s.clone(), leaked);
87                return leaked.to_string();
88            }
89        }
90
91        s
92    }
93
94    /// Fast character classification using lookup table
95    fn is_valid_separator(c: char) -> bool {
96        matches!(c, '.' | '-' | '_' | '+')
97    }
98
99    /// Fast alphanumeric check with underscore support
100    fn is_token_char(c: char) -> bool {
101        c.is_ascii_alphanumeric() || c == '_'
102    }
103
104    /// Parse version string using zero-copy state machine
105    #[allow(clippy::type_complexity)]
106    #[allow(clippy::missing_errors_doc)]
107    pub fn parse_tokens(
108        &self,
109        input: &str,
110    ) -> Result<(SmallVec<[TokenType; 8]>, SmallVec<[char; 7]>), RezCoreError> {
111        if input.is_empty() {
112            return Ok((SmallVec::new(), SmallVec::new()));
113        }
114
115        let mut tokens = SmallVec::new();
116        let mut separators = SmallVec::new();
117        let mut state = ParseState::Start;
118        let mut current_token = String::new();
119        let mut numeric_count = 0;
120
121        let chars: SmallVec<[char; 64]> = input.chars().collect();
122        let mut i = 0;
123
124        while i < chars.len() {
125            let c = chars[i];
126
127            match state {
128                ParseState::Start => {
129                    if Self::is_token_char(c) {
130                        current_token.push(c);
131                        state = ParseState::InToken;
132                    } else if Self::is_valid_separator(c) {
133                        return Err(RezCoreError::VersionParse(format!(
134                            "Version cannot start with separator '{c}'"
135                        )));
136                    } else {
137                        return Err(RezCoreError::VersionParse(format!(
138                            "Invalid character '{c}' at start of version"
139                        )));
140                    }
141                }
142
143                ParseState::InToken => {
144                    if Self::is_token_char(c) {
145                        current_token.push(c);
146                    } else if Self::is_valid_separator(c) {
147                        // Finalize current token
148                        self.finalize_token(&mut current_token, &mut tokens, &mut numeric_count)?;
149                        separators.push(c);
150                        state = ParseState::InSeparator;
151                    } else {
152                        return Err(RezCoreError::VersionParse(format!(
153                            "Invalid character '{c}' in token"
154                        )));
155                    }
156                }
157
158                ParseState::InSeparator => {
159                    if Self::is_token_char(c) {
160                        current_token.push(c);
161                        state = ParseState::InToken;
162                    } else {
163                        return Err(RezCoreError::VersionParse(format!(
164                            "Expected token character after separator, found '{c}'"
165                        )));
166                    }
167                }
168            }
169
170            i += 1;
171        }
172
173        // Finalize last token if we're in a token state
174        if state == ParseState::InToken && !current_token.is_empty() {
175            self.finalize_token(&mut current_token, &mut tokens, &mut numeric_count)?;
176        } else if state == ParseState::InSeparator {
177            return Err(RezCoreError::VersionParse(
178                "Version cannot end with separator".to_string(),
179            ));
180        }
181
182        // Validate token counts
183        if tokens.len() > self.max_tokens {
184            return Err(RezCoreError::VersionParse(format!(
185                "Too many tokens: {} (max: {})",
186                tokens.len(),
187                self.max_tokens
188            )));
189        }
190
191        if numeric_count > self.max_numeric_tokens {
192            return Err(RezCoreError::VersionParse(format!(
193                "Too many numeric tokens: {} (max: {})",
194                numeric_count, self.max_numeric_tokens
195            )));
196        }
197
198        Ok((tokens, separators))
199    }
200
201    /// Finalize a token and add it to the tokens list
202    #[allow(clippy::missing_errors_doc)]
203    fn finalize_token(
204        &self,
205        current_token: &mut String,
206        tokens: &mut SmallVec<[TokenType; 8]>,
207        numeric_count: &mut usize,
208    ) -> Result<(), RezCoreError> {
209        if current_token.is_empty() {
210            return Err(RezCoreError::VersionParse("Empty token found".to_string()));
211        }
212
213        // Validate token format
214        if current_token.starts_with('_') || current_token.ends_with('_') {
215            return Err(RezCoreError::VersionParse(format!(
216                "Invalid token format: '{current_token}'"
217            )));
218        }
219
220        // Check for invalid patterns
221        if current_token == "not" || current_token == "version" {
222            return Err(RezCoreError::VersionParse(format!(
223                "Invalid version token: '{current_token}'"
224            )));
225        }
226
227        // Reject overly long alphabetic tokens
228        if current_token.chars().all(char::is_alphabetic) && current_token.len() > 10 {
229            return Err(RezCoreError::VersionParse(format!(
230                "Invalid version token: '{current_token}'"
231            )));
232        }
233
234        // Try to parse as numeric first (fast path)
235        if current_token.chars().all(|c| c.is_ascii_digit()) {
236            if let Ok(num) = current_token.parse::<u64>() {
237                tokens.push(TokenType::Numeric(num));
238                *numeric_count += 1;
239            } else {
240                // Number too large, treat as alphanumeric
241                let interned = self.intern_string(current_token.clone());
242                tokens.push(TokenType::Alphanumeric(interned));
243            }
244        } else {
245            // Alphanumeric token
246            let interned = self.intern_string(current_token.clone());
247            tokens.push(TokenType::Alphanumeric(interned));
248        }
249
250        current_token.clear();
251        Ok(())
252    }
253}
254
255/// Legacy `VersionParser` for backward compatibility
256pub struct VersionParser {
257    _inner: StateMachineParser,
258}
259
260impl VersionParser {
261    /// Create a new parser
262    #[must_use]
263    pub fn new() -> Self {
264        Self {
265            _inner: StateMachineParser::new(),
266        }
267    }
268
269    /// Parse a complete version string
270    #[allow(clippy::missing_errors_doc)]
271    pub fn parse_version(&self, input: &str) -> Result<Version, RezCoreError> {
272        // Use the new state machine parser for better performance
273        // but fall back to the original implementation for now
274        Version::parse(input)
275    }
276}
277
278impl Default for VersionParser {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284impl Default for StateMachineParser {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_parser_creation() {
296        let _parser = VersionParser::new();
297        let _state_machine_parser = StateMachineParser::new();
298        // Verify parsers can be created without panicking
299    }
300
301    #[test]
302    fn test_state_machine_parser_basic() {
303        let parser = StateMachineParser::new();
304
305        // Test empty input
306        let (tokens, separators) = parser.parse_tokens("").unwrap();
307        assert!(tokens.is_empty());
308        assert!(separators.is_empty());
309
310        // Test simple version
311        let (tokens, separators) = parser.parse_tokens("1.2.3").unwrap();
312        assert_eq!(tokens.len(), 3);
313        assert_eq!(separators.len(), 2);
314
315        // Check token types
316        match &tokens[0] {
317            TokenType::Numeric(n) => assert_eq!(*n, 1),
318            _ => panic!("Expected numeric token"),
319        }
320
321        assert_eq!(separators[0], '.');
322        assert_eq!(separators[1], '.');
323    }
324
325    #[test]
326    fn test_state_machine_parser_alphanumeric() {
327        let parser = StateMachineParser::new();
328
329        let (tokens, separators) = parser.parse_tokens("1.2.3-alpha1").unwrap();
330        assert_eq!(tokens.len(), 4);
331        assert_eq!(separators.len(), 3);
332
333        // Check mixed token types
334        match &tokens[0] {
335            TokenType::Numeric(n) => assert_eq!(*n, 1),
336            _ => panic!("Expected numeric token"),
337        }
338
339        match &tokens[3] {
340            TokenType::Alphanumeric(s) => assert_eq!(s, "alpha1"),
341            _ => panic!("Expected alphanumeric token"),
342        }
343    }
344
345    #[test]
346    fn test_state_machine_parser_errors() {
347        let parser = StateMachineParser::new();
348
349        // Test invalid start
350        assert!(parser.parse_tokens(".1.2.3").is_err());
351
352        // Test invalid end
353        assert!(parser.parse_tokens("1.2.3.").is_err());
354
355        // Test invalid characters
356        assert!(parser.parse_tokens("1.2.3@").is_err());
357
358        // Test invalid token patterns
359        assert!(parser.parse_tokens("_invalid").is_err());
360        assert!(parser.parse_tokens("invalid_").is_err());
361    }
362
363    #[test]
364    fn test_string_interning() {
365        let parser = StateMachineParser::with_config(true, 10, 5);
366
367        // Parse the same version multiple times
368        let (tokens1, _) = parser.parse_tokens("1.0.0-alpha").unwrap();
369        let (tokens2, _) = parser.parse_tokens("1.0.0-alpha").unwrap();
370
371        // String interning should work for alphanumeric tokens
372        if let (TokenType::Alphanumeric(s1), TokenType::Alphanumeric(s2)) =
373            (&tokens1[3], &tokens2[3])
374        {
375            // Note: We can't directly test pointer equality due to the way we handle interning
376            assert_eq!(s1, s2);
377        }
378    }
379
380    #[test]
381    fn test_performance_limits() {
382        let parser = StateMachineParser::new();
383
384        // Test max tokens limit
385        let too_many_tokens = (0..15).map(|i| i.to_string()).collect::<Vec<_>>().join(".");
386        assert!(parser.parse_tokens(&too_many_tokens).is_err());
387
388        // Test max numeric tokens limit
389        let too_many_numeric = (0..10).map(|i| i.to_string()).collect::<Vec<_>>().join(".");
390        assert!(parser.parse_tokens(&too_many_numeric).is_err());
391    }
392
393    #[test]
394    fn test_character_classification() {
395        assert!(StateMachineParser::is_valid_separator('.'));
396        assert!(StateMachineParser::is_valid_separator('-'));
397        assert!(StateMachineParser::is_valid_separator('_'));
398        assert!(StateMachineParser::is_valid_separator('+'));
399        assert!(!StateMachineParser::is_valid_separator('@'));
400
401        assert!(StateMachineParser::is_token_char('a'));
402        assert!(StateMachineParser::is_token_char('1'));
403        assert!(StateMachineParser::is_token_char('_'));
404        assert!(!StateMachineParser::is_token_char('.'));
405    }
406}