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(i64),
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    // A literal too large for i64 becomes a real, per PLRM: "an integer that
512    // would exceed this limit is automatically converted to a real value".
513    if let Ok(v) = s.parse::<i64>() {
514        return Some(Token::Int(v));
515    }
516    s.parse::<f64>().ok().map(Token::Real)
517}
518
519/// Try to parse a radix number: `base#digits`.
520fn try_parse_radix(token: &[u8]) -> Option<Token> {
521    let s = std::str::from_utf8(token).ok()?;
522    let hash_pos = s.find('#')?;
523    let base_str = &s[..hash_pos];
524    let digits_str = &s[hash_pos + 1..];
525    if digits_str.is_empty() {
526        return None;
527    }
528    let base: u32 = base_str.parse().ok()?;
529    if !(2..=36).contains(&base) {
530        return None;
531    }
532    // Radix literals are unsigned in source form; `16#FFFFFFFF` is a positive
533    // value that fits i64, so the whole parsed range stays integral.
534    let value = i64::from_str_radix(digits_str, base).ok()?;
535    Some(Token::Int(value))
536}
537
538// ─── Streaming tokenizer (byte-at-a-time from FileStore) ────────────────────
539
540/// Read the next token from a file/filter stream, one byte at a time.
541///
542/// Returns the token and the number of newlines consumed in leading
543/// whitespace/comments. Returns `None` on EOF.
544pub fn stream_next_token(
545    files: &mut FileStore,
546    entity: EntityId,
547) -> Result<Option<(Token, u32)>, PsError> {
548    let mut newlines = 0u32;
549
550    // Skip whitespace and comments to find the first significant byte.
551    let first = loop {
552        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
553            None => return Ok(None),
554            Some(b) if is_whitespace(b) => {
555                if b == b'\n' || b == b'\r' {
556                    newlines += 1;
557                }
558                continue;
559            }
560            Some(b'%') => {
561                // Skip comment until end of line.
562                loop {
563                    match files.read_byte(entity).map_err(|_| PsError::IOError)? {
564                        None => return Ok(None),
565                        Some(b'\n') | Some(b'\r') => {
566                            newlines += 1;
567                            break;
568                        }
569                        Some(_) => {}
570                    }
571                }
572                continue;
573            }
574            Some(b) => break b,
575        }
576    };
577
578    let token = match first {
579        b'(' => stream_scan_string(files, entity)?,
580        b'<' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
581            Some(b'<') => Token::DictBegin,
582            Some(b'~') => stream_scan_ascii85(files, entity)?,
583            other => {
584                if let Some(b) = other {
585                    files.putback_bytes(entity, &[b]);
586                }
587                stream_scan_hex_string(files, entity)?
588            }
589        },
590        b'>' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
591            Some(b'>') => Token::DictEnd,
592            _ => return Err(PsError::SyntaxError),
593        },
594        b'{' => Token::ProcBegin,
595        b'}' => Token::ProcEnd,
596        b'[' => Token::ArrayBegin,
597        b']' => Token::ArrayEnd,
598        b'/' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
599            Some(b'/') => {
600                let name = stream_read_name_bytes(files, entity)?;
601                Token::ImmediateName(name)
602            }
603            Some(b) if !is_whitespace(b) && !is_delimiter(b) => {
604                files.putback_bytes(entity, &[b]);
605                let name = stream_read_name_bytes(files, entity)?;
606                Token::LiteralName(name)
607            }
608            other => {
609                if let Some(b) = other {
610                    files.putback_bytes(entity, &[b]);
611                }
612                Token::LiteralName(Vec::new())
613            }
614        },
615        // Binary token bytes
616        128..=159 => Token::BinaryTokenByte(first),
617        _ => {
618            // Number or executable name — collect bytes until delimiter.
619            let mut token_bytes = vec![first];
620            loop {
621                match files.read_byte(entity).map_err(|_| PsError::IOError)? {
622                    None => break,
623                    Some(b) if is_whitespace(b) => {
624                        // PLRM: trailing whitespace consumed for numbers and
625                        // executable names.  Critical for `RD` followed by
626                        // binary charstring data.
627                        if b == b'\n' || b == b'\r' {
628                            newlines += 1;
629                        }
630                        break;
631                    }
632                    Some(b) if is_delimiter(b) => {
633                        files.putback_bytes(entity, &[b]);
634                        break;
635                    }
636                    // Binary token bytes terminate names/numbers
637                    Some(b @ 128..=159) => {
638                        files.putback_bytes(entity, &[b]);
639                        break;
640                    }
641                    Some(b) => token_bytes.push(b),
642                }
643            }
644            try_parse_number_token(&token_bytes).unwrap_or(Token::Name(token_bytes, true))
645        }
646    };
647
648    Ok(Some((token, newlines)))
649}
650
651/// Read name bytes from a stream until whitespace or delimiter.
652/// Always puts back the terminating byte (literal/immediate names
653/// do NOT consume trailing whitespace per PLRM).
654fn stream_read_name_bytes(files: &mut FileStore, entity: EntityId) -> Result<Vec<u8>, PsError> {
655    let mut name = Vec::new();
656    loop {
657        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
658            None => break,
659            Some(b) if is_whitespace(b) || is_delimiter(b) || is_binary_token_byte(b) => {
660                files.putback_bytes(entity, &[b]);
661                break;
662            }
663            Some(b) => name.push(b),
664        }
665    }
666    Ok(name)
667}
668
669/// Scan a parenthesised string from a stream: `(...)`.
670fn stream_scan_string(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
671    let mut result = Vec::new();
672    let mut depth: u32 = 1;
673
674    while depth > 0 {
675        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
676            None => return Err(PsError::SyntaxError),
677            Some(b'(') => {
678                depth += 1;
679                result.push(b'(');
680            }
681            Some(b')') => {
682                depth -= 1;
683                if depth > 0 {
684                    result.push(b')');
685                }
686            }
687            Some(b'\\') => {
688                let esc = files
689                    .read_byte(entity)
690                    .map_err(|_| PsError::IOError)?
691                    .ok_or(PsError::SyntaxError)?;
692                match esc {
693                    b'n' => result.push(b'\n'),
694                    b'r' => result.push(b'\r'),
695                    b't' => result.push(b'\t'),
696                    b'b' => result.push(0x08),
697                    b'f' => result.push(0x0C),
698                    b'\\' => result.push(b'\\'),
699                    b'(' => result.push(b'('),
700                    b')' => result.push(b')'),
701                    b'\n' => {} // line continuation
702                    b'\r' => {
703                        // \r\n is a single line continuation.
704                        if let Some(next) = files.read_byte(entity).map_err(|_| PsError::IOError)?
705                            && next != b'\n'
706                        {
707                            files.putback_bytes(entity, &[next]);
708                        }
709                    }
710                    b'0'..=b'7' => {
711                        let mut val = esc - b'0';
712                        for _ in 0..2 {
713                            match files.read_byte(entity).map_err(|_| PsError::IOError)? {
714                                Some(b @ b'0'..=b'7') => val = (val << 3) | (b - b'0'),
715                                Some(other) => {
716                                    files.putback_bytes(entity, &[other]);
717                                    break;
718                                }
719                                None => break,
720                            }
721                        }
722                        result.push(val);
723                    }
724                    _ => result.push(esc),
725                }
726            }
727            Some(b) => result.push(b),
728        }
729    }
730
731    Ok(Token::String(result))
732}
733
734/// Scan a hex string from a stream: `<...>`.
735fn stream_scan_hex_string(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
736    let mut result = Vec::new();
737    let mut nibble: Option<u8> = None;
738
739    loop {
740        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
741            None => return Err(PsError::SyntaxError),
742            Some(b'>') => {
743                if let Some(high) = nibble {
744                    result.push(high << 4);
745                }
746                return Ok(Token::String(result));
747            }
748            Some(b) if is_whitespace(b) => continue,
749            Some(b) => {
750                let digit = match b {
751                    b'0'..=b'9' => b - b'0',
752                    b'a'..=b'f' => b - b'a' + 10,
753                    b'A'..=b'F' => b - b'A' + 10,
754                    _ => return Err(PsError::SyntaxError),
755                };
756                match nibble {
757                    None => nibble = Some(digit),
758                    Some(high) => {
759                        result.push((high << 4) | digit);
760                        nibble = None;
761                    }
762                }
763            }
764        }
765    }
766}
767
768/// Scan an ASCII85 string from a stream: `<~...~>`.
769fn stream_scan_ascii85(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
770    let mut encoded = Vec::new();
771
772    loop {
773        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
774            None => return Err(PsError::SyntaxError),
775            Some(b'~') => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
776                Some(b'>') => return Ok(Token::String(decode_ascii85(&encoded)?)),
777                _ => return Err(PsError::SyntaxError),
778            },
779            Some(b) if is_whitespace(b) => continue,
780            Some(b) => encoded.push(b),
781        }
782    }
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788
789    fn tokenize_all(input: &[u8]) -> Vec<Token> {
790        let mut t = Tokenizer::new(input);
791        let mut tokens = Vec::new();
792        while let Ok(Some(tok)) = t.next_token() {
793            tokens.push(tok);
794        }
795        tokens
796    }
797
798    #[test]
799    fn test_integers() {
800        assert_eq!(tokenize_all(b"42"), vec![Token::Int(42)]);
801        assert_eq!(tokenize_all(b"-7"), vec![Token::Int(-7)]);
802        assert_eq!(tokenize_all(b"+3"), vec![Token::Int(3)]);
803        assert_eq!(tokenize_all(b"0"), vec![Token::Int(0)]);
804    }
805
806    #[test]
807    fn test_reals() {
808        assert_eq!(tokenize_all(b"2.5"), vec![Token::Real(2.5)]);
809        assert_eq!(tokenize_all(b"-0.5"), vec![Token::Real(-0.5)]);
810        assert_eq!(tokenize_all(b"1e10"), vec![Token::Real(1e10)]);
811        assert_eq!(tokenize_all(b"1.5E-3"), vec![Token::Real(1.5e-3)]);
812    }
813
814    #[test]
815    fn test_radix() {
816        assert_eq!(tokenize_all(b"16#FF"), vec![Token::Int(255)]);
817        assert_eq!(tokenize_all(b"2#1010"), vec![Token::Int(10)]);
818        assert_eq!(tokenize_all(b"8#77"), vec![Token::Int(63)]);
819    }
820
821    #[test]
822    fn test_names() {
823        assert_eq!(
824            tokenize_all(b"add"),
825            vec![Token::Name(b"add".to_vec(), true)]
826        );
827        assert_eq!(
828            tokenize_all(b"/foo"),
829            vec![Token::LiteralName(b"foo".to_vec())]
830        );
831        assert_eq!(
832            tokenize_all(b"//bar"),
833            vec![Token::ImmediateName(b"bar".to_vec())]
834        );
835    }
836
837    #[test]
838    fn test_string_basic() {
839        assert_eq!(
840            tokenize_all(b"(hello)"),
841            vec![Token::String(b"hello".to_vec())]
842        );
843    }
844
845    #[test]
846    fn test_string_escapes() {
847        assert_eq!(
848            tokenize_all(b"(a\\nb)"),
849            vec![Token::String(b"a\nb".to_vec())]
850        );
851        assert_eq!(
852            tokenize_all(b"(a\\\\b)"),
853            vec![Token::String(b"a\\b".to_vec())]
854        );
855        assert_eq!(
856            tokenize_all(b"(\\110\\145\\154\\154\\157)"),
857            vec![Token::String(b"Hello".to_vec())]
858        );
859    }
860
861    #[test]
862    fn test_string_balanced_parens() {
863        assert_eq!(
864            tokenize_all(b"(a(b)c)"),
865            vec![Token::String(b"a(b)c".to_vec())]
866        );
867    }
868
869    #[test]
870    fn test_hex_string() {
871        assert_eq!(
872            tokenize_all(b"<48656C6C6F>"),
873            vec![Token::String(b"Hello".to_vec())]
874        );
875        // Odd nibble padded
876        assert_eq!(tokenize_all(b"<0>"), vec![Token::String(vec![0x00])]);
877    }
878
879    #[test]
880    fn test_procedures() {
881        let tokens = tokenize_all(b"{ add }");
882        assert_eq!(
883            tokens,
884            vec![
885                Token::ProcBegin,
886                Token::Name(b"add".to_vec(), true),
887                Token::ProcEnd,
888            ]
889        );
890    }
891
892    #[test]
893    fn test_comments() {
894        let tokens = tokenize_all(b"3 % comment\n4 add");
895        assert_eq!(
896            tokens,
897            vec![
898                Token::Int(3),
899                Token::Int(4),
900                Token::Name(b"add".to_vec(), true),
901            ]
902        );
903    }
904
905    #[test]
906    fn test_full_program() {
907        let tokens = tokenize_all(b"3 4 add 7 eq { (YES\\n) print } { (NO\\n) print } ifelse");
908        assert_eq!(tokens.len(), 14);
909        assert_eq!(tokens[0], Token::Int(3));
910        assert_eq!(tokens[1], Token::Int(4));
911        assert_eq!(tokens[2], Token::Name(b"add".to_vec(), true));
912        assert_eq!(tokens[3], Token::Int(7));
913        assert_eq!(tokens[4], Token::Name(b"eq".to_vec(), true));
914        assert_eq!(tokens[5], Token::ProcBegin);
915        assert_eq!(tokens[6], Token::String(b"YES\n".to_vec()));
916        assert_eq!(tokens[7], Token::Name(b"print".to_vec(), true));
917        assert_eq!(tokens[8], Token::ProcEnd);
918        assert_eq!(tokens[9], Token::ProcBegin);
919        assert_eq!(tokens[10], Token::String(b"NO\n".to_vec()));
920        assert_eq!(tokens[11], Token::Name(b"print".to_vec(), true));
921        assert_eq!(tokens[12], Token::ProcEnd);
922        assert_eq!(tokens[13], Token::Name(b"ifelse".to_vec(), true));
923    }
924
925    #[test]
926    fn test_dict_delimiters() {
927        let tokens = tokenize_all(b"<< /foo 42 >>");
928        assert_eq!(
929            tokens,
930            vec![
931                Token::DictBegin,
932                Token::LiteralName(b"foo".to_vec()),
933                Token::Int(42),
934                Token::DictEnd,
935            ]
936        );
937    }
938}