Skip to main content

stet_core/
tokenizer.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! PostScript tokenizer.
6//!
7//! Converts a byte stream into a sequence of tokens following the PLRM
8//! tokenization rules: numbers, names, strings, hex strings, procedures.
9
10use crate::error::PsError;
11use crate::file_store::FileStore;
12use crate::object::EntityId;
13
14/// A single PostScript token.
15#[derive(Debug, Clone, PartialEq)]
16pub enum Token {
17    Int(i32),
18    Real(f64),
19    Name(Vec<u8>, bool),    // (bytes, is_executable)
20    LiteralName(Vec<u8>),   // /name
21    ImmediateName(Vec<u8>), // //name
22    String(Vec<u8>),        // (hello) or <hex>
23    ProcBegin,              // {
24    ProcEnd,                // }
25    ArrayBegin,             // [
26    ArrayEnd,               // ]
27    DictBegin,              // <<
28    DictEnd,                // >>
29    /// Binary token byte (128-159) — caller must invoke the binary token parser.
30    BinaryTokenByte(u8),
31    Eof,
32}
33
34/// PostScript tokenizer.
35pub struct Tokenizer<'a> {
36    input: &'a [u8],
37    pos: usize,
38}
39
40impl<'a> Tokenizer<'a> {
41    pub fn new(input: &'a [u8]) -> Self {
42        Self { input, pos: 0 }
43    }
44
45    /// Current byte position in the input.
46    pub fn position(&self) -> usize {
47        self.pos
48    }
49
50    /// Return the remaining input bytes starting at the given position.
51    pub fn remaining_from(&self, pos: usize) -> &[u8] {
52        &self.input[pos..]
53    }
54
55    /// Advance the position by the given number of bytes.
56    pub fn advance(&mut self, n: usize) {
57        self.pos += n;
58    }
59
60    /// Return the next token, or `None` at EOF.
61    pub fn next_token(&mut self) -> Result<Option<Token>, PsError> {
62        self.skip_whitespace_and_comments();
63
64        if self.pos >= self.input.len() {
65            return Ok(None);
66        }
67
68        let b = self.input[self.pos];
69        match b {
70            b'(' => self.scan_string().map(Some),
71            b'<' => {
72                if self.pos + 1 < self.input.len() && self.input[self.pos + 1] == b'<' {
73                    self.pos += 2;
74                    Ok(Some(Token::DictBegin))
75                } else if self.pos + 1 < self.input.len() && self.input[self.pos + 1] == b'~' {
76                    self.scan_ascii85_string().map(Some)
77                } else {
78                    self.scan_hex_string().map(Some)
79                }
80            }
81            b'>' => {
82                if self.pos + 1 < self.input.len() && self.input[self.pos + 1] == b'>' {
83                    self.pos += 2;
84                    Ok(Some(Token::DictEnd))
85                } else {
86                    Err(PsError::SyntaxError)
87                }
88            }
89            b'{' => {
90                self.pos += 1;
91                Ok(Some(Token::ProcBegin))
92            }
93            b'}' => {
94                self.pos += 1;
95                Ok(Some(Token::ProcEnd))
96            }
97            b'[' => {
98                self.pos += 1;
99                Ok(Some(Token::ArrayBegin))
100            }
101            b']' => {
102                self.pos += 1;
103                Ok(Some(Token::ArrayEnd))
104            }
105            b'/' => {
106                self.pos += 1;
107                if self.pos < self.input.len() && self.input[self.pos] == b'/' {
108                    self.pos += 1;
109                    Ok(Some(self.scan_immediate_name()))
110                } else {
111                    Ok(Some(self.scan_literal_name()))
112                }
113            }
114            // Binary token bytes
115            128..=159 => {
116                self.pos += 1;
117                Ok(Some(Token::BinaryTokenByte(b)))
118            }
119            _ => {
120                // Try number first, fall back to name
121                if let Some(tok) = self.try_scan_number() {
122                    Ok(Some(tok))
123                } else {
124                    Ok(Some(self.scan_name()))
125                }
126            }
127        }
128    }
129
130    fn skip_whitespace_and_comments(&mut self) {
131        while self.pos < self.input.len() {
132            let b = self.input[self.pos];
133            if Self::is_whitespace(b) {
134                self.pos += 1;
135            } else if b == b'%' {
136                // Skip to end of line
137                while self.pos < self.input.len()
138                    && self.input[self.pos] != b'\n'
139                    && self.input[self.pos] != b'\r'
140                {
141                    self.pos += 1;
142                }
143            } else {
144                break;
145            }
146        }
147    }
148
149    /// Try to scan a number. Returns `None` if the token at pos is not a valid number.
150    fn try_scan_number(&mut self) -> Option<Token> {
151        let start = self.pos;
152        let bytes = self.input;
153        let len = bytes.len();
154
155        if start >= len {
156            return None;
157        }
158
159        // Collect the token (up to whitespace, delimiter, or binary token byte)
160        let mut end = start;
161        while end < len
162            && !Self::is_whitespace(bytes[end])
163            && !Self::is_delimiter(bytes[end])
164            && !is_binary_token_byte(bytes[end])
165        {
166            end += 1;
167        }
168
169        if end == start {
170            return None;
171        }
172
173        let token_bytes = &bytes[start..end];
174
175        if let Some(tok) = try_parse_number_token(token_bytes) {
176            self.pos = end;
177            Some(tok)
178        } else {
179            None
180        }
181    }
182
183    fn scan_name(&mut self) -> Token {
184        let start = self.pos;
185        while self.pos < self.input.len()
186            && !Self::is_whitespace(self.input[self.pos])
187            && !Self::is_delimiter(self.input[self.pos])
188            && !is_binary_token_byte(self.input[self.pos])
189        {
190            self.pos += 1;
191        }
192        let name = self.input[start..self.pos].to_vec();
193        Token::Name(name, true) // executable name
194    }
195
196    fn scan_literal_name(&mut self) -> Token {
197        let start = self.pos;
198        while self.pos < self.input.len()
199            && !Self::is_whitespace(self.input[self.pos])
200            && !Self::is_delimiter(self.input[self.pos])
201            && !is_binary_token_byte(self.input[self.pos])
202        {
203            self.pos += 1;
204        }
205        let name = self.input[start..self.pos].to_vec();
206        Token::LiteralName(name)
207    }
208
209    fn scan_immediate_name(&mut self) -> Token {
210        let start = self.pos;
211        while self.pos < self.input.len()
212            && !Self::is_whitespace(self.input[self.pos])
213            && !Self::is_delimiter(self.input[self.pos])
214            && !is_binary_token_byte(self.input[self.pos])
215        {
216            self.pos += 1;
217        }
218        let name = self.input[start..self.pos].to_vec();
219        Token::ImmediateName(name)
220    }
221
222    /// Scan a parenthesized string: `(...)` with escape handling and balanced parens.
223    fn scan_string(&mut self) -> Result<Token, PsError> {
224        self.pos += 1; // skip opening '('
225        let mut result = Vec::new();
226        let mut depth = 1;
227
228        while self.pos < self.input.len() && depth > 0 {
229            let b = self.input[self.pos];
230            match b {
231                b'(' => {
232                    depth += 1;
233                    result.push(b'(');
234                    self.pos += 1;
235                }
236                b')' => {
237                    depth -= 1;
238                    if depth > 0 {
239                        result.push(b')');
240                    }
241                    self.pos += 1;
242                }
243                b'\\' => {
244                    self.pos += 1;
245                    if self.pos >= self.input.len() {
246                        return Err(PsError::SyntaxError);
247                    }
248                    let esc = self.input[self.pos];
249                    match esc {
250                        b'n' => {
251                            result.push(b'\n');
252                            self.pos += 1;
253                        }
254                        b'r' => {
255                            result.push(b'\r');
256                            self.pos += 1;
257                        }
258                        b't' => {
259                            result.push(b'\t');
260                            self.pos += 1;
261                        }
262                        b'b' => {
263                            result.push(0x08);
264                            self.pos += 1;
265                        }
266                        b'f' => {
267                            result.push(0x0C);
268                            self.pos += 1;
269                        }
270                        b'\\' => {
271                            result.push(b'\\');
272                            self.pos += 1;
273                        }
274                        b'(' => {
275                            result.push(b'(');
276                            self.pos += 1;
277                        }
278                        b')' => {
279                            result.push(b')');
280                            self.pos += 1;
281                        }
282                        b'\n' => {
283                            // Line continuation — skip
284                            self.pos += 1;
285                        }
286                        b'\r' => {
287                            // Line continuation — skip (and skip \n if follows)
288                            self.pos += 1;
289                            if self.pos < self.input.len() && self.input[self.pos] == b'\n' {
290                                self.pos += 1;
291                            }
292                        }
293                        b'0'..=b'7' => {
294                            // Octal escape: 1-3 digits
295                            let mut val: u8 = esc - b'0';
296                            self.pos += 1;
297                            for _ in 0..2 {
298                                if self.pos < self.input.len()
299                                    && self.input[self.pos] >= b'0'
300                                    && self.input[self.pos] <= b'7'
301                                {
302                                    val = (val << 3) | (self.input[self.pos] - b'0');
303                                    self.pos += 1;
304                                } else {
305                                    break;
306                                }
307                            }
308                            result.push(val);
309                        }
310                        _ => {
311                            // Unrecognized escape: just the char itself
312                            result.push(esc);
313                            self.pos += 1;
314                        }
315                    }
316                }
317                _ => {
318                    result.push(b);
319                    self.pos += 1;
320                }
321            }
322        }
323
324        if depth != 0 {
325            return Err(PsError::SyntaxError);
326        }
327
328        Ok(Token::String(result))
329    }
330
331    /// Scan a hex string: `<...>`.
332    fn scan_hex_string(&mut self) -> Result<Token, PsError> {
333        self.pos += 1; // skip '<'
334        let mut result = Vec::new();
335        let mut nibble: Option<u8> = None;
336
337        while self.pos < self.input.len() {
338            let b = self.input[self.pos];
339            if b == b'>' {
340                self.pos += 1;
341                // If we have a pending nibble, pad with 0
342                if let Some(high) = nibble {
343                    result.push(high << 4);
344                }
345                return Ok(Token::String(result));
346            }
347
348            if Self::is_whitespace(b) {
349                self.pos += 1;
350                continue;
351            }
352
353            let digit = match b {
354                b'0'..=b'9' => b - b'0',
355                b'a'..=b'f' => b - b'a' + 10,
356                b'A'..=b'F' => b - b'A' + 10,
357                _ => return Err(PsError::SyntaxError),
358            };
359
360            match nibble {
361                None => nibble = Some(digit),
362                Some(high) => {
363                    result.push((high << 4) | digit);
364                    nibble = None;
365                }
366            }
367            self.pos += 1;
368        }
369
370        Err(PsError::SyntaxError) // unterminated hex string
371    }
372
373    /// Scan an ASCII85 string: `<~...~>`.
374    fn scan_ascii85_string(&mut self) -> Result<Token, PsError> {
375        self.pos += 2; // skip '<~'
376        let mut encoded = Vec::new();
377
378        while self.pos < self.input.len() {
379            let b = self.input[self.pos];
380            if b == b'~' {
381                self.pos += 1;
382                if self.pos < self.input.len() && self.input[self.pos] == b'>' {
383                    self.pos += 1;
384                    return Ok(Token::String(Self::decode_ascii85(&encoded)?));
385                }
386                return Err(PsError::SyntaxError);
387            }
388            if !Self::is_whitespace(b) {
389                encoded.push(b);
390            }
391            self.pos += 1;
392        }
393
394        Err(PsError::SyntaxError)
395    }
396
397    fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PsError> {
398        decode_ascii85(data)
399    }
400
401    fn is_whitespace(b: u8) -> bool {
402        is_whitespace(b)
403    }
404
405    fn is_delimiter(b: u8) -> bool {
406        is_delimiter(b)
407    }
408}
409
410// ─── Standalone helpers (shared by slice-based and streaming tokenizers) ─────
411
412/// PostScript whitespace: all bytes ≤ 0x20.
413fn is_whitespace(b: u8) -> bool {
414    b <= b' '
415}
416
417/// Binary token byte (128-159) — terminates names and numbers.
418fn is_binary_token_byte(b: u8) -> bool {
419    (128..=159).contains(&b)
420}
421
422/// PostScript delimiter characters.
423fn is_delimiter(b: u8) -> bool {
424    matches!(
425        b,
426        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
427    )
428}
429
430/// Decode an ASCII85-encoded byte sequence.
431fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PsError> {
432    let mut result = Vec::new();
433    let mut i = 0;
434
435    while i < data.len() {
436        if data[i] == b'z' {
437            result.extend_from_slice(&[0, 0, 0, 0]);
438            i += 1;
439            continue;
440        }
441
442        let mut group = [0u8; 5];
443        let mut count = 0;
444        while count < 5 && i < data.len() && data[i] != b'z' {
445            if data[i] < b'!' || data[i] > b'u' {
446                return Err(PsError::SyntaxError);
447            }
448            group[count] = data[i] - b'!';
449            count += 1;
450            i += 1;
451        }
452
453        if count < 2 {
454            if count == 1 {
455                return Err(PsError::SyntaxError);
456            }
457            break;
458        }
459
460        // Pad remaining with 'u' (84)
461        for g in group.iter_mut().skip(count) {
462            *g = 84;
463        }
464
465        let mut value: u32 = 0;
466        for &g in &group {
467            value = value
468                .checked_mul(85)
469                .and_then(|v| v.checked_add(g as u32))
470                .ok_or(PsError::SyntaxError)?;
471        }
472
473        let bytes = value.to_be_bytes();
474        let output_count = count - 1;
475        result.extend_from_slice(&bytes[..output_count]);
476    }
477
478    Ok(result)
479}
480
481/// Try to parse a byte sequence as a PostScript number token.
482fn try_parse_number_token(token_bytes: &[u8]) -> Option<Token> {
483    if token_bytes.is_empty() {
484        return None;
485    }
486
487    // Try radix: base#digits
488    if let Some(result) = try_parse_radix(token_bytes) {
489        return Some(result);
490    }
491
492    let s = std::str::from_utf8(token_bytes).ok()?;
493
494    let first = token_bytes[0];
495    let looks_numeric = first.is_ascii_digit()
496        || ((first == b'+' || first == b'-')
497            && token_bytes.len() > 1
498            && (token_bytes[1].is_ascii_digit() || token_bytes[1] == b'.'))
499        || (first == b'.' && token_bytes.len() > 1 && token_bytes[1].is_ascii_digit());
500
501    if !looks_numeric {
502        return None;
503    }
504
505    let is_real = s.contains('.') || s.contains('e') || s.contains('E');
506
507    if is_real {
508        return s.parse::<f64>().ok().map(Token::Real);
509    }
510
511    if let Ok(v) = s.parse::<i32>() {
512        return Some(Token::Int(v));
513    }
514    if let Ok(v) = s.parse::<i64>() {
515        return Some(Token::Real(v as f64));
516    }
517    s.parse::<f64>().ok().map(Token::Real)
518}
519
520/// Try to parse a radix number: `base#digits`.
521fn try_parse_radix(token: &[u8]) -> Option<Token> {
522    let s = std::str::from_utf8(token).ok()?;
523    let hash_pos = s.find('#')?;
524    let base_str = &s[..hash_pos];
525    let digits_str = &s[hash_pos + 1..];
526    if digits_str.is_empty() {
527        return None;
528    }
529    let base: u32 = base_str.parse().ok()?;
530    if !(2..=36).contains(&base) {
531        return None;
532    }
533    let value = i64::from_str_radix(digits_str, base).ok()?;
534    if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
535        Some(Token::Int(value as i32))
536    } else {
537        Some(Token::Real(value as f64))
538    }
539}
540
541// ─── Streaming tokenizer (byte-at-a-time from FileStore) ────────────────────
542
543/// Read the next token from a file/filter stream, one byte at a time.
544///
545/// Returns the token and the number of newlines consumed in leading
546/// whitespace/comments. Returns `None` on EOF.
547pub fn stream_next_token(
548    files: &mut FileStore,
549    entity: EntityId,
550) -> Result<Option<(Token, u32)>, PsError> {
551    let mut newlines = 0u32;
552
553    // Skip whitespace and comments to find the first significant byte.
554    let first = loop {
555        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
556            None => return Ok(None),
557            Some(b) if is_whitespace(b) => {
558                if b == b'\n' || b == b'\r' {
559                    newlines += 1;
560                }
561                continue;
562            }
563            Some(b'%') => {
564                // Skip comment until end of line.
565                loop {
566                    match files.read_byte(entity).map_err(|_| PsError::IOError)? {
567                        None => return Ok(None),
568                        Some(b'\n') | Some(b'\r') => {
569                            newlines += 1;
570                            break;
571                        }
572                        Some(_) => {}
573                    }
574                }
575                continue;
576            }
577            Some(b) => break b,
578        }
579    };
580
581    let token = match first {
582        b'(' => stream_scan_string(files, entity)?,
583        b'<' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
584            Some(b'<') => Token::DictBegin,
585            Some(b'~') => stream_scan_ascii85(files, entity)?,
586            other => {
587                if let Some(b) = other {
588                    files.putback_bytes(entity, &[b]);
589                }
590                stream_scan_hex_string(files, entity)?
591            }
592        },
593        b'>' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
594            Some(b'>') => Token::DictEnd,
595            _ => return Err(PsError::SyntaxError),
596        },
597        b'{' => Token::ProcBegin,
598        b'}' => Token::ProcEnd,
599        b'[' => Token::ArrayBegin,
600        b']' => Token::ArrayEnd,
601        b'/' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
602            Some(b'/') => {
603                let name = stream_read_name_bytes(files, entity)?;
604                Token::ImmediateName(name)
605            }
606            Some(b) if !is_whitespace(b) && !is_delimiter(b) => {
607                files.putback_bytes(entity, &[b]);
608                let name = stream_read_name_bytes(files, entity)?;
609                Token::LiteralName(name)
610            }
611            other => {
612                if let Some(b) = other {
613                    files.putback_bytes(entity, &[b]);
614                }
615                Token::LiteralName(Vec::new())
616            }
617        },
618        // Binary token bytes
619        128..=159 => Token::BinaryTokenByte(first),
620        _ => {
621            // Number or executable name — collect bytes until delimiter.
622            let mut token_bytes = vec![first];
623            loop {
624                match files.read_byte(entity).map_err(|_| PsError::IOError)? {
625                    None => break,
626                    Some(b) if is_whitespace(b) => {
627                        // PLRM: trailing whitespace consumed for numbers and
628                        // executable names.  Critical for `RD` followed by
629                        // binary charstring data.
630                        if b == b'\n' || b == b'\r' {
631                            newlines += 1;
632                        }
633                        break;
634                    }
635                    Some(b) if is_delimiter(b) => {
636                        files.putback_bytes(entity, &[b]);
637                        break;
638                    }
639                    // Binary token bytes terminate names/numbers
640                    Some(b @ 128..=159) => {
641                        files.putback_bytes(entity, &[b]);
642                        break;
643                    }
644                    Some(b) => token_bytes.push(b),
645                }
646            }
647            try_parse_number_token(&token_bytes).unwrap_or(Token::Name(token_bytes, true))
648        }
649    };
650
651    Ok(Some((token, newlines)))
652}
653
654/// Read name bytes from a stream until whitespace or delimiter.
655/// Always puts back the terminating byte (literal/immediate names
656/// do NOT consume trailing whitespace per PLRM).
657fn stream_read_name_bytes(files: &mut FileStore, entity: EntityId) -> Result<Vec<u8>, PsError> {
658    let mut name = Vec::new();
659    loop {
660        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
661            None => break,
662            Some(b) if is_whitespace(b) || is_delimiter(b) || is_binary_token_byte(b) => {
663                files.putback_bytes(entity, &[b]);
664                break;
665            }
666            Some(b) => name.push(b),
667        }
668    }
669    Ok(name)
670}
671
672/// Scan a parenthesised string from a stream: `(...)`.
673fn stream_scan_string(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
674    let mut result = Vec::new();
675    let mut depth: u32 = 1;
676
677    while depth > 0 {
678        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
679            None => return Err(PsError::SyntaxError),
680            Some(b'(') => {
681                depth += 1;
682                result.push(b'(');
683            }
684            Some(b')') => {
685                depth -= 1;
686                if depth > 0 {
687                    result.push(b')');
688                }
689            }
690            Some(b'\\') => {
691                let esc = files
692                    .read_byte(entity)
693                    .map_err(|_| PsError::IOError)?
694                    .ok_or(PsError::SyntaxError)?;
695                match esc {
696                    b'n' => result.push(b'\n'),
697                    b'r' => result.push(b'\r'),
698                    b't' => result.push(b'\t'),
699                    b'b' => result.push(0x08),
700                    b'f' => result.push(0x0C),
701                    b'\\' => result.push(b'\\'),
702                    b'(' => result.push(b'('),
703                    b')' => result.push(b')'),
704                    b'\n' => {} // line continuation
705                    b'\r' => {
706                        // \r\n is a single line continuation.
707                        if let Some(next) = files.read_byte(entity).map_err(|_| PsError::IOError)?
708                            && next != b'\n'
709                        {
710                            files.putback_bytes(entity, &[next]);
711                        }
712                    }
713                    b'0'..=b'7' => {
714                        let mut val = esc - b'0';
715                        for _ in 0..2 {
716                            match files.read_byte(entity).map_err(|_| PsError::IOError)? {
717                                Some(b @ b'0'..=b'7') => val = (val << 3) | (b - b'0'),
718                                Some(other) => {
719                                    files.putback_bytes(entity, &[other]);
720                                    break;
721                                }
722                                None => break,
723                            }
724                        }
725                        result.push(val);
726                    }
727                    _ => result.push(esc),
728                }
729            }
730            Some(b) => result.push(b),
731        }
732    }
733
734    Ok(Token::String(result))
735}
736
737/// Scan a hex string from a stream: `<...>`.
738fn stream_scan_hex_string(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
739    let mut result = Vec::new();
740    let mut nibble: Option<u8> = None;
741
742    loop {
743        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
744            None => return Err(PsError::SyntaxError),
745            Some(b'>') => {
746                if let Some(high) = nibble {
747                    result.push(high << 4);
748                }
749                return Ok(Token::String(result));
750            }
751            Some(b) if is_whitespace(b) => continue,
752            Some(b) => {
753                let digit = match b {
754                    b'0'..=b'9' => b - b'0',
755                    b'a'..=b'f' => b - b'a' + 10,
756                    b'A'..=b'F' => b - b'A' + 10,
757                    _ => return Err(PsError::SyntaxError),
758                };
759                match nibble {
760                    None => nibble = Some(digit),
761                    Some(high) => {
762                        result.push((high << 4) | digit);
763                        nibble = None;
764                    }
765                }
766            }
767        }
768    }
769}
770
771/// Scan an ASCII85 string from a stream: `<~...~>`.
772fn stream_scan_ascii85(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
773    let mut encoded = Vec::new();
774
775    loop {
776        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
777            None => return Err(PsError::SyntaxError),
778            Some(b'~') => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
779                Some(b'>') => return Ok(Token::String(decode_ascii85(&encoded)?)),
780                _ => return Err(PsError::SyntaxError),
781            },
782            Some(b) if is_whitespace(b) => continue,
783            Some(b) => encoded.push(b),
784        }
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    fn tokenize_all(input: &[u8]) -> Vec<Token> {
793        let mut t = Tokenizer::new(input);
794        let mut tokens = Vec::new();
795        while let Ok(Some(tok)) = t.next_token() {
796            tokens.push(tok);
797        }
798        tokens
799    }
800
801    #[test]
802    fn test_integers() {
803        assert_eq!(tokenize_all(b"42"), vec![Token::Int(42)]);
804        assert_eq!(tokenize_all(b"-7"), vec![Token::Int(-7)]);
805        assert_eq!(tokenize_all(b"+3"), vec![Token::Int(3)]);
806        assert_eq!(tokenize_all(b"0"), vec![Token::Int(0)]);
807    }
808
809    #[test]
810    fn test_reals() {
811        assert_eq!(tokenize_all(b"2.5"), vec![Token::Real(2.5)]);
812        assert_eq!(tokenize_all(b"-0.5"), vec![Token::Real(-0.5)]);
813        assert_eq!(tokenize_all(b"1e10"), vec![Token::Real(1e10)]);
814        assert_eq!(tokenize_all(b"1.5E-3"), vec![Token::Real(1.5e-3)]);
815    }
816
817    #[test]
818    fn test_radix() {
819        assert_eq!(tokenize_all(b"16#FF"), vec![Token::Int(255)]);
820        assert_eq!(tokenize_all(b"2#1010"), vec![Token::Int(10)]);
821        assert_eq!(tokenize_all(b"8#77"), vec![Token::Int(63)]);
822    }
823
824    #[test]
825    fn test_names() {
826        assert_eq!(
827            tokenize_all(b"add"),
828            vec![Token::Name(b"add".to_vec(), true)]
829        );
830        assert_eq!(
831            tokenize_all(b"/foo"),
832            vec![Token::LiteralName(b"foo".to_vec())]
833        );
834        assert_eq!(
835            tokenize_all(b"//bar"),
836            vec![Token::ImmediateName(b"bar".to_vec())]
837        );
838    }
839
840    #[test]
841    fn test_string_basic() {
842        assert_eq!(
843            tokenize_all(b"(hello)"),
844            vec![Token::String(b"hello".to_vec())]
845        );
846    }
847
848    #[test]
849    fn test_string_escapes() {
850        assert_eq!(
851            tokenize_all(b"(a\\nb)"),
852            vec![Token::String(b"a\nb".to_vec())]
853        );
854        assert_eq!(
855            tokenize_all(b"(a\\\\b)"),
856            vec![Token::String(b"a\\b".to_vec())]
857        );
858        assert_eq!(
859            tokenize_all(b"(\\110\\145\\154\\154\\157)"),
860            vec![Token::String(b"Hello".to_vec())]
861        );
862    }
863
864    #[test]
865    fn test_string_balanced_parens() {
866        assert_eq!(
867            tokenize_all(b"(a(b)c)"),
868            vec![Token::String(b"a(b)c".to_vec())]
869        );
870    }
871
872    #[test]
873    fn test_hex_string() {
874        assert_eq!(
875            tokenize_all(b"<48656C6C6F>"),
876            vec![Token::String(b"Hello".to_vec())]
877        );
878        // Odd nibble padded
879        assert_eq!(tokenize_all(b"<0>"), vec![Token::String(vec![0x00])]);
880    }
881
882    #[test]
883    fn test_procedures() {
884        let tokens = tokenize_all(b"{ add }");
885        assert_eq!(
886            tokens,
887            vec![
888                Token::ProcBegin,
889                Token::Name(b"add".to_vec(), true),
890                Token::ProcEnd,
891            ]
892        );
893    }
894
895    #[test]
896    fn test_comments() {
897        let tokens = tokenize_all(b"3 % comment\n4 add");
898        assert_eq!(
899            tokens,
900            vec![
901                Token::Int(3),
902                Token::Int(4),
903                Token::Name(b"add".to_vec(), true),
904            ]
905        );
906    }
907
908    #[test]
909    fn test_full_program() {
910        let tokens = tokenize_all(b"3 4 add 7 eq { (YES\\n) print } { (NO\\n) print } ifelse");
911        assert_eq!(tokens.len(), 14);
912        assert_eq!(tokens[0], Token::Int(3));
913        assert_eq!(tokens[1], Token::Int(4));
914        assert_eq!(tokens[2], Token::Name(b"add".to_vec(), true));
915        assert_eq!(tokens[3], Token::Int(7));
916        assert_eq!(tokens[4], Token::Name(b"eq".to_vec(), true));
917        assert_eq!(tokens[5], Token::ProcBegin);
918        assert_eq!(tokens[6], Token::String(b"YES\n".to_vec()));
919        assert_eq!(tokens[7], Token::Name(b"print".to_vec(), true));
920        assert_eq!(tokens[8], Token::ProcEnd);
921        assert_eq!(tokens[9], Token::ProcBegin);
922        assert_eq!(tokens[10], Token::String(b"NO\n".to_vec()));
923        assert_eq!(tokens[11], Token::Name(b"print".to_vec(), true));
924        assert_eq!(tokens[12], Token::ProcEnd);
925        assert_eq!(tokens[13], Token::Name(b"ifelse".to_vec(), true));
926    }
927
928    #[test]
929    fn test_dict_delimiters() {
930        let tokens = tokenize_all(b"<< /foo 42 >>");
931        assert_eq!(
932            tokens,
933            vec![
934                Token::DictBegin,
935                Token::LiteralName(b"foo".to_vec()),
936                Token::Int(42),
937                Token::DictEnd,
938            ]
939        );
940    }
941}