Skip to main content

serializer/
tokenizer.rs

1//! Zero-copy tokenizer for DX Machine format
2//!
3//! Uses SIMD-accelerated scanning via memchr for maximum performance.
4//! Operates directly on byte slices without allocations.
5
6use crate::error::{DxError, Result};
7use crate::utf8::validate_string_input;
8use memchr::memchr;
9
10/// Token types in DX format
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Token<'a> {
13    /// End of input
14    Eof,
15    /// Newline
16    Newline,
17    /// Colon (:)
18    Colon,
19    /// Equals (=) - Schema definition
20    Equals,
21    /// Caret (^) - Prefix inheritance
22    Caret,
23    /// Pipe (|) - Array delimiter
24    Pipe,
25    /// Greater than (>) - Stream operator
26    Stream,
27    /// Underscore (_) - Ditto
28    Ditto,
29    /// Tilde (~) - Null
30    Null,
31    /// Plus (+) - True
32    True,
33    /// Minus (-) - False
34    False,
35    /// Bang (!) - Implicit true flag
36    Bang,
37    /// Question (?) - Implicit null
38    Void,
39    /// Dollar ($) - Alias definition
40    Dollar,
41    /// At (@) - Anchor reference
42    At,
43    /// Percent (%) - Type hint
44    Percent,
45    /// Dot (.) - Path separator
46    Dot,
47    /// Identifier/String
48    Ident(&'a [u8]),
49    /// Integer
50    Int(i64),
51    /// Float
52    Float(f64),
53}
54
55/// Zero-copy tokenizer
56pub struct Tokenizer<'a> {
57    pub(crate) input: &'a [u8],
58    pos: usize,
59}
60
61impl<'a> Tokenizer<'a> {
62    /// Create new tokenizer from bytes
63    pub fn new(input: &'a [u8]) -> Self {
64        Self { input, pos: 0 }
65    }
66
67    /// Get current position
68    pub fn pos(&self) -> usize {
69        self.pos
70    }
71
72    /// Reset to a saved position
73    pub fn reset_to(&mut self, pos: usize) {
74        self.pos = pos;
75    }
76
77    /// Check if at end
78    pub fn is_eof(&self) -> bool {
79        self.pos >= self.input.len()
80    }
81
82    /// Peek current byte without advancing
83    pub fn peek(&self) -> Option<u8> {
84        self.input.get(self.pos).copied()
85    }
86
87    /// Peek n bytes ahead
88    pub fn peek_n(&self, n: usize) -> Option<u8> {
89        self.input.get(self.pos + n).copied()
90    }
91
92    /// Advance by n bytes
93    pub fn advance(&mut self, n: usize) {
94        self.pos = (self.pos + n).min(self.input.len());
95    }
96
97    /// Skip whitespace (space and tab only, not newlines)
98    pub fn skip_whitespace(&mut self) {
99        while let Some(b) = self.peek() {
100            if b == b' ' || b == b'\t' {
101                self.pos += 1;
102            } else {
103                break;
104            }
105        }
106    }
107
108    /// Skip to end of line (for comments)
109    pub fn skip_line(&mut self) {
110        while let Some(b) = self.peek() {
111            self.pos += 1;
112            if b == b'\n' {
113                break;
114            }
115        }
116    }
117
118    /// Read until delimiter using SIMD search
119    pub fn read_until(&mut self, delim: u8) -> &'a [u8] {
120        let start = self.pos;
121        let remaining = &self.input[self.pos..];
122
123        match memchr(delim, remaining) {
124            Some(offset) => {
125                self.pos += offset;
126                &self.input[start..self.pos]
127            }
128            None => {
129                self.pos = self.input.len();
130                &self.input[start..]
131            }
132        }
133    }
134
135    /// Read until any of the delimiters
136    pub fn read_until_any(&mut self, delims: &[u8]) -> &'a [u8] {
137        let start = self.pos;
138        while let Some(b) = self.peek() {
139            if delims.contains(&b) {
140                break;
141            }
142            self.pos += 1;
143        }
144        &self.input[start..self.pos]
145    }
146
147    /// Read identifier (alphanumeric + underscore)
148    pub fn read_ident(&mut self) -> &'a [u8] {
149        let start = self.pos;
150        while let Some(b) = self.peek() {
151            if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'-' {
152                self.pos += 1;
153            } else {
154                break;
155            }
156        }
157        &self.input[start..self.pos]
158    }
159
160    /// Read number (int or float)
161    pub fn read_number(&mut self) -> Result<Token<'a>> {
162        let start = self.pos;
163        let mut has_dot = false;
164        let mut has_exp = false;
165
166        // Handle negative sign
167        if self.peek() == Some(b'-') {
168            self.pos += 1;
169        }
170
171        // Read digits, dots, and exponents
172        while let Some(b) = self.peek() {
173            match b {
174                b'0'..=b'9' => self.pos += 1,
175                b'.' if !has_dot && !has_exp => {
176                    has_dot = true;
177                    self.pos += 1;
178                }
179                b'e' | b'E' if !has_exp => {
180                    has_exp = true;
181                    self.pos += 1;
182                    // Handle optional +/- after exponent
183                    if matches!(self.peek(), Some(b'+') | Some(b'-')) {
184                        self.pos += 1;
185                    }
186                }
187                _ => break,
188            }
189        }
190
191        let num_bytes = &self.input[start..self.pos];
192        let num_str = std::str::from_utf8(num_bytes)
193            .map_err(|_| DxError::InvalidNumber("Invalid UTF-8 in number".to_string()))?;
194
195        if has_dot || has_exp {
196            let val = num_str
197                .parse::<f64>()
198                .map_err(|_| DxError::InvalidNumber(num_str.to_string()))?;
199            Ok(Token::Float(val))
200        } else {
201            let val = num_str
202                .parse::<i64>()
203                .map_err(|_| DxError::InvalidNumber(num_str.to_string()))?;
204            Ok(Token::Int(val))
205        }
206    }
207
208    /// Read string value using vacuum parsing
209    /// Reads until the next expected type based on schema
210    pub fn read_string_vacuum(&mut self, next_is_number: bool) -> &'a [u8] {
211        let start = self.pos;
212
213        if next_is_number {
214            // Read until we hit a number or delimiter
215            while let Some(b) = self.peek() {
216                if b.is_ascii_digit() || b == b'-' || b == b'|' || b == b'\n' {
217                    break;
218                }
219                self.pos += 1;
220            }
221        } else {
222            // Read until delimiter or boolean marker
223            // Stop at + or - (boolean markers) when preceded by whitespace
224            while let Some(b) = self.peek() {
225                if b == b'|' || b == b'\n' || b == b'#' {
226                    break;
227                }
228                // Check for boolean markers preceded by whitespace
229                // Only stop if we've read at least one character and the previous char is whitespace
230                if (b == b'+' || b == b'-') && self.pos > start {
231                    let prev = self.input[self.pos - 1];
232                    if prev.is_ascii_whitespace() {
233                        break;
234                    }
235                }
236                self.pos += 1;
237            }
238        }
239
240        // Trim trailing whitespace
241        let mut end = self.pos;
242        while end > start && self.input[end - 1].is_ascii_whitespace() {
243            end -= 1;
244        }
245
246        &self.input[start..end]
247    }
248
249    /// Read string value with UTF-8 validation
250    /// Returns an error if the string contains invalid UTF-8
251    pub fn read_string_validated(&mut self, next_is_number: bool) -> Result<&'a str> {
252        let start = self.pos;
253        let bytes = self.read_string_vacuum(next_is_number);
254        validate_string_input(bytes, start)
255    }
256
257    /// Read identifier with UTF-8 validation
258    pub fn read_ident_validated(&mut self) -> Result<&'a str> {
259        let start = self.pos;
260        let bytes = self.read_ident();
261        validate_string_input(bytes, start)
262    }
263
264    /// Get next token
265    pub fn next_token(&mut self) -> Result<Token<'a>> {
266        self.skip_whitespace();
267
268        let b = match self.peek() {
269            Some(b) => b,
270            None => return Ok(Token::Eof),
271        };
272
273        // Check for single-char tokens
274        let token = match b {
275            b'\n' | b'\r' => {
276                self.pos += 1;
277                if b == b'\r' && self.peek() == Some(b'\n') {
278                    self.pos += 1;
279                }
280                return Ok(Token::Newline);
281            }
282            b'#' => {
283                self.skip_line();
284                return self.next_token(); // Skip comment and get next
285            }
286            b':' => {
287                self.pos += 1;
288                Token::Colon
289            }
290            b'=' => {
291                self.pos += 1;
292                Token::Equals
293            }
294            b'^' => {
295                self.pos += 1;
296                Token::Caret
297            }
298            b'|' => {
299                self.pos += 1;
300                Token::Pipe
301            }
302            b'>' => {
303                self.pos += 1;
304                Token::Stream
305            }
306            b'_' => {
307                self.pos += 1;
308                Token::Ditto
309            }
310            b'~' => {
311                self.pos += 1;
312                Token::Null
313            }
314            b'+' => {
315                self.pos += 1;
316                Token::True
317            }
318            b'-' if !matches!(self.peek_n(1), Some(b'0'..=b'9')) => {
319                self.pos += 1;
320                Token::False
321            }
322            b'!' => {
323                self.pos += 1;
324                Token::Bang
325            }
326            b'?' => {
327                self.pos += 1;
328                Token::Void
329            }
330            b'$' => {
331                self.pos += 1;
332                Token::Dollar
333            }
334            b'@' => {
335                self.pos += 1;
336                Token::At
337            }
338            b'%' => {
339                self.pos += 1;
340                Token::Percent
341            }
342            b'.' => {
343                self.pos += 1;
344                Token::Dot
345            }
346            b'0'..=b'9' | b'-' => {
347                return self.read_number();
348            }
349            _ => {
350                let ident = self.read_ident();
351                Token::Ident(ident)
352            }
353        };
354
355        Ok(token)
356    }
357
358    /// Peek next token without consuming
359    pub fn peek_token(&self) -> Result<Token<'a>> {
360        let mut temp = Self {
361            input: self.input,
362            pos: self.pos,
363        };
364        temp.next_token()
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_tokenizer_basic() {
374        let input = b"id:123 name:Test active:+ score:9.5";
375        let mut tok = Tokenizer::new(input);
376
377        assert_eq!(tok.next_token().unwrap(), Token::Ident(b"id"));
378        assert_eq!(tok.next_token().unwrap(), Token::Colon);
379        assert_eq!(tok.next_token().unwrap(), Token::Int(123));
380        assert_eq!(tok.next_token().unwrap(), Token::Ident(b"name"));
381        assert_eq!(tok.next_token().unwrap(), Token::Colon);
382    }
383
384    #[test]
385    fn test_special_tokens() {
386        let input = b"_ ~ + - ^ | > = $ @ % !";
387        let mut tok = Tokenizer::new(input);
388
389        assert_eq!(tok.next_token().unwrap(), Token::Ditto);
390        assert_eq!(tok.next_token().unwrap(), Token::Null);
391        assert_eq!(tok.next_token().unwrap(), Token::True);
392        assert_eq!(tok.next_token().unwrap(), Token::False);
393        assert_eq!(tok.next_token().unwrap(), Token::Caret);
394        assert_eq!(tok.next_token().unwrap(), Token::Pipe);
395        assert_eq!(tok.next_token().unwrap(), Token::Stream);
396        assert_eq!(tok.next_token().unwrap(), Token::Equals);
397    }
398
399    #[test]
400    #[allow(clippy::approx_constant)] // Using 3.14 intentionally for test data
401    fn test_numbers() {
402        let input = b"123 -456 3.14 -2.5 1e10";
403        let mut tok = Tokenizer::new(input);
404
405        assert_eq!(tok.next_token().unwrap(), Token::Int(123));
406        assert_eq!(tok.next_token().unwrap(), Token::Int(-456));
407        assert_eq!(tok.next_token().unwrap(), Token::Float(3.14));
408        assert_eq!(tok.next_token().unwrap(), Token::Float(-2.5));
409    }
410}