Skip to main content

openbim_step/
lexer.rs

1//! Byte-level tokenizer for STEP physical files.
2//!
3//! The tokenizer accepts arbitrary bytes because the file structure is ASCII
4//! and legacy producers may place non-UTF-8 bytes inside string literals.
5
6use crate::{Span, Spanned, StepError};
7use std::borrow::Cow;
8
9/// One lexical unit in a physical file.
10#[derive(Debug, Clone, PartialEq)]
11pub enum Token<'a> {
12    /// `#42`.
13    Id(Cow<'a, [u8]>),
14    /// A quoted string body, still escaped.
15    Text(Cow<'a, [u8]>),
16    /// A binary literal body.
17    Binary(Cow<'a, [u8]>),
18    /// A dotted keyword or enumeration name, without dots.
19    Keyword(Cow<'a, [u8]>),
20    /// A bare identifier or physical-file marker.
21    Name(Cow<'a, [u8]>),
22    /// Integer lexical bytes.
23    Integer(Cow<'a, [u8]>),
24    /// Real lexical bytes.
25    Real(Cow<'a, [u8]>),
26    /// `$`.
27    Dollar,
28    /// `*`.
29    Star,
30    /// `(`.
31    OpenParen,
32    /// `)`.
33    CloseParen,
34    /// `,`.
35    Comma,
36    /// `=`.
37    Equals,
38    /// `;`.
39    Semicolon,
40}
41
42/// Streaming tokenizer over a byte slice.
43#[derive(Debug, Clone)]
44pub struct Lexer<'a> {
45    input: &'a [u8],
46    position: usize,
47    finished: bool,
48}
49
50fn is_valid_binary(body: &[u8]) -> bool {
51    matches!(body.first(), Some(b'0'..=b'3')) && body[1..].iter().all(u8::is_ascii_hexdigit)
52}
53
54const fn is_ignored_control(byte: u8) -> bool {
55    matches!(byte, b'\t' | b'\n' | b'\r' | 0x0c)
56}
57
58fn strip_print_directives(bytes: Cow<'_, [u8]>) -> Cow<'_, [u8]> {
59    if !bytes
60        .windows(3)
61        .any(|window| matches!(window, b"\\N\\" | b"\\F\\"))
62    {
63        return bytes;
64    }
65    let mut stripped = Vec::with_capacity(bytes.len());
66    let mut position = 0;
67    while position < bytes.len() {
68        if matches!(bytes.get(position..position + 3), Some(b"\\N\\" | b"\\F\\")) {
69            position += 3;
70        } else {
71            stripped.push(bytes[position]);
72            position += 1;
73        }
74    }
75    Cow::Owned(stripped)
76}
77
78fn strip_text_print_directives(bytes: Cow<'_, [u8]>) -> Cow<'_, [u8]> {
79    let mut stripped: Option<Vec<u8>> = None;
80    let mut position = 0;
81    while position < bytes.len() {
82        if bytes.get(position..position + 2) == Some(b"\\\\") {
83            if let Some(output) = &mut stripped {
84                output.extend_from_slice(b"\\\\");
85            }
86            position += 2;
87        } else if matches!(bytes.get(position..position + 3), Some(b"\\N\\" | b"\\F\\")) {
88            stripped.get_or_insert_with(|| {
89                let mut output = Vec::with_capacity(bytes.len());
90                output.extend_from_slice(&bytes[..position]);
91                output
92            });
93            position += 3;
94        } else {
95            if let Some(output) = &mut stripped {
96                output.push(bytes[position]);
97            }
98            position += 1;
99        }
100    }
101    stripped.map_or(bytes, Cow::Owned)
102}
103
104impl<'a> Lexer<'a> {
105    /// Starts tokenizing `input`.
106    #[must_use]
107    pub const fn new(input: &'a [u8]) -> Self {
108        Self {
109            input,
110            position: 0,
111            finished: false,
112        }
113    }
114
115    /// Current byte offset.
116    #[must_use]
117    pub const fn offset(&self) -> usize {
118        self.position
119    }
120
121    /// Restarts tokenization at `position`.
122    ///
123    /// Only bounded error recovery uses this: after a lexical failure the
124    /// parser must step past the damaged bytes to resynchronize. Callers are
125    /// responsible for advancing monotonically, otherwise recovery can loop.
126    pub(crate) fn resume_at(&mut self, position: usize) {
127        self.position = position.min(self.input.len());
128        self.finished = false;
129    }
130
131    fn skip_ignored_controls(&mut self) {
132        while self
133            .input
134            .get(self.position)
135            .is_some_and(|byte| is_ignored_control(*byte))
136        {
137            self.position += 1;
138        }
139    }
140
141    fn token_bytes(&self, start: usize, end: usize) -> Cow<'a, [u8]> {
142        let bytes = &self.input[start..end];
143        if bytes.iter().any(|byte| is_ignored_control(*byte)) {
144            Cow::Owned(
145                bytes
146                    .iter()
147                    .copied()
148                    .filter(|byte| !is_ignored_control(*byte))
149                    .collect(),
150            )
151        } else {
152            Cow::Borrowed(bytes)
153        }
154    }
155
156    fn match_ignoring_controls(&self, start: usize, expected: &[u8]) -> Option<usize> {
157        let mut position = start;
158        for &expected_byte in expected {
159            while self
160                .input
161                .get(position)
162                .is_some_and(|byte| is_ignored_control(*byte))
163            {
164                position += 1;
165            }
166            if self.input.get(position) != Some(&expected_byte) {
167                return None;
168            }
169            position += 1;
170        }
171        Some(position)
172    }
173
174    fn match_ignoring_text_controls(&self, start: usize, expected: &[u8]) -> Option<usize> {
175        let mut position = start;
176        for &expected_byte in expected {
177            loop {
178                while self
179                    .input
180                    .get(position)
181                    .is_some_and(|byte| is_ignored_control(*byte))
182                {
183                    position += 1;
184                }
185                let Some(end) = self
186                    .match_ignoring_controls(position, b"\\N\\")
187                    .or_else(|| self.match_ignoring_controls(position, b"\\F\\"))
188                else {
189                    break;
190                };
191                position = end;
192            }
193            if self.input.get(position) != Some(&expected_byte) {
194                return None;
195            }
196            position += 1;
197        }
198        Some(position)
199    }
200
201    /// Produces the next spanned token.
202    /// # Errors
203    ///
204    /// Returns a syntax diagnostic for malformed literals, comments, numbers,
205    /// or bytes that are not STEP punctuation.
206    pub fn next_spanned(&mut self) -> Result<Option<Spanned<Token<'a>>>, StepError> {
207        if self.finished {
208            return Ok(None);
209        }
210        self.skip_trivia()?;
211        let Some(&byte) = self.input.get(self.position) else {
212            self.finished = true;
213            return Ok(None);
214        };
215        let start = self.position;
216        let value = match byte {
217            b'(' => self.single(Token::OpenParen),
218            b')' => self.single(Token::CloseParen),
219            b',' => self.single(Token::Comma),
220            b'=' => self.single(Token::Equals),
221            b';' => self.single(Token::Semicolon),
222            b'$' => self.single(Token::Dollar),
223            b'*' => self.single(Token::Star),
224            b'#' => self.lex_id(start)?,
225            b'!' => self.lex_user_defined_name(start)?,
226            b'\'' => self.lex_text(start)?,
227            b'"' => self.lex_binary(start)?,
228            b'.' if self.input.get(start + 1).is_some_and(u8::is_ascii_digit) => {
229                self.lex_number(start)?
230            }
231            b'.' => self.lex_keyword(start)?,
232            b'0'..=b'9' | b'-' | b'+' => self.lex_number(start)?,
233            value if value.is_ascii_alphabetic() || value == b'_' => self.lex_name(),
234            _ => {
235                self.position += 1;
236                return Err(StepError::syntax(
237                    Span::new(start, self.position),
238                    format!("unexpected byte 0x{byte:02X}"),
239                ));
240            }
241        };
242        Ok(Some(Spanned::new(value, Span::new(start, self.position))))
243    }
244
245    /// Produces the next spanned token.
246    ///
247    /// This is an alias for [`Lexer::next_spanned`].
248    ///
249    /// # Errors
250    ///
251    /// Returns a syntax diagnostic for malformed input.
252    pub fn next_token(&mut self) -> Result<Option<Spanned<Token<'a>>>, StepError> {
253        self.next_spanned()
254    }
255
256    fn skip_trivia(&mut self) -> Result<(), StepError> {
257        if self.position == 0 && self.input.starts_with(&[0xef, 0xbb, 0xbf]) {
258            self.position = 3;
259        }
260        loop {
261            while self
262                .input
263                .get(self.position)
264                .is_some_and(u8::is_ascii_whitespace)
265            {
266                self.position += 1;
267            }
268            if let Some(end) = self
269                .match_ignoring_controls(self.position, b"\\N\\")
270                .or_else(|| self.match_ignoring_controls(self.position, b"\\F\\"))
271            {
272                self.position = end;
273                continue;
274            }
275            if let Some(body_start) = self.match_ignoring_controls(self.position, b"/*") {
276                let start = self.position;
277                let mut cursor = body_start;
278                loop {
279                    if let Some(end) = self.match_ignoring_controls(cursor, b"*/") {
280                        self.position = end;
281                        break;
282                    }
283                    if cursor >= self.input.len() {
284                        self.position = self.input.len();
285                        return Err(StepError::syntax(
286                            Span::new(start, self.position),
287                            "unterminated comment",
288                        ));
289                    }
290                    cursor += 1;
291                }
292                continue;
293            }
294            return Ok(());
295        }
296    }
297
298    fn single(&mut self, token: Token<'a>) -> Token<'a> {
299        self.position += 1;
300        token
301    }
302
303    fn lex_id(&mut self, start: usize) -> Result<Token<'a>, StepError> {
304        self.position += 1;
305        self.skip_ignored_controls();
306        let digits = self.position;
307        let mut saw_digit = false;
308        while let Some(&byte) = self.input.get(self.position) {
309            if is_ignored_control(byte) {
310                self.position += 1;
311            } else if byte.is_ascii_digit() {
312                saw_digit = true;
313                self.position += 1;
314            } else {
315                break;
316            }
317        }
318        if !saw_digit {
319            return Err(StepError::syntax(
320                Span::new(start, self.position),
321                "expected digits after '#'",
322            ));
323        }
324        Ok(Token::Id(self.token_bytes(digits, self.position)))
325    }
326
327    fn lex_text(&mut self, start: usize) -> Result<Token<'a>, StepError> {
328        self.position += 1;
329        let body_start = self.position;
330        while let Some(&byte) = self.input.get(self.position) {
331            if byte == b'\'' {
332                if let Some(end) = self.match_ignoring_text_controls(self.position, b"''") {
333                    self.position = end;
334                    continue;
335                }
336                let text = strip_text_print_directives(self.token_bytes(body_start, self.position));
337                self.position += 1;
338                return Ok(Token::Text(text));
339            }
340            self.position += 1;
341        }
342        Err(StepError::syntax(
343            Span::new(start, self.position),
344            "unterminated string literal",
345        ))
346    }
347
348    fn lex_binary(&mut self, start: usize) -> Result<Token<'a>, StepError> {
349        self.position += 1;
350        let body_start = self.position;
351        while let Some(&byte) = self.input.get(self.position) {
352            if byte == b'"' {
353                let body = strip_print_directives(self.token_bytes(body_start, self.position));
354                self.position += 1;
355                if !is_valid_binary(body.as_ref()) {
356                    return Err(StepError::syntax(
357                        Span::new(start, self.position),
358                        "invalid binary literal",
359                    ));
360                }
361                return Ok(Token::Binary(body));
362            }
363            self.position += 1;
364        }
365        Err(StepError::syntax(
366            Span::new(start, self.position),
367            "unterminated binary literal",
368        ))
369    }
370
371    fn lex_keyword(&mut self, start: usize) -> Result<Token<'a>, StepError> {
372        self.position += 1;
373        let body_start = self.position;
374        while let Some(&byte) = self.input.get(self.position) {
375            if is_ignored_control(byte) {
376                self.position += 1;
377                continue;
378            }
379            if byte == b'.' {
380                let body = self.token_bytes(body_start, self.position);
381                if body.is_empty() {
382                    self.position += 1;
383                    return Err(StepError::syntax(
384                        Span::new(start, self.position),
385                        "empty dotted keyword",
386                    ));
387                }
388                self.position += 1;
389                return Ok(Token::Keyword(body));
390            }
391            if !(byte.is_ascii_alphanumeric() || byte == b'_') {
392                break;
393            }
394            self.position += 1;
395        }
396        Err(StepError::syntax(
397            Span::new(start, self.position),
398            "unterminated dotted keyword",
399        ))
400    }
401
402    fn lex_name(&mut self) -> Token<'a> {
403        let start = self.position;
404        while let Some(&byte) = self.input.get(self.position) {
405            if is_ignored_control(byte)
406                || byte.is_ascii_alphanumeric()
407                || matches!(byte, b'_' | b'-')
408            {
409                self.position += 1;
410            } else {
411                break;
412            }
413        }
414        Token::Name(self.token_bytes(start, self.position))
415    }
416
417    fn lex_user_defined_name(&mut self, start: usize) -> Result<Token<'a>, StepError> {
418        self.position += 1;
419        self.skip_ignored_controls();
420        let body_start = self.position;
421        if !self
422            .input
423            .get(body_start)
424            .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
425        {
426            return Err(StepError::syntax(
427                Span::new(start, self.position),
428                "invalid user-defined keyword",
429            ));
430        }
431        self.position += 1;
432        while let Some(&byte) = self.input.get(self.position) {
433            if is_ignored_control(byte) || byte.is_ascii_alphanumeric() || byte == b'_' {
434                self.position += 1;
435            } else {
436                break;
437            }
438        }
439        if matches!(self.input.get(self.position), Some(b'-')) {
440            return Err(StepError::syntax(
441                Span::new(start, self.position + 1),
442                "invalid user-defined keyword",
443            ));
444        }
445        Ok(Token::Name(self.token_bytes(start, self.position)))
446    }
447
448    fn lex_number(&mut self, start: usize) -> Result<Token<'a>, StepError> {
449        if matches!(self.input.get(self.position), Some(b'+' | b'-')) {
450            self.position += 1;
451            self.skip_ignored_controls();
452        }
453        let mut integer_digits = 0;
454        loop {
455            self.skip_ignored_controls();
456            if self
457                .input
458                .get(self.position)
459                .is_some_and(u8::is_ascii_digit)
460            {
461                integer_digits += 1;
462                self.position += 1;
463            } else {
464                break;
465            }
466        }
467        if integer_digits == 0 {
468            return Err(StepError::syntax(
469                Span::new(start, self.position),
470                "number has no leading digits",
471            ));
472        }
473        self.skip_ignored_controls();
474        let mut real = false;
475        if self.input.get(self.position) == Some(&b'.') {
476            real = true;
477            self.position += 1;
478            loop {
479                self.skip_ignored_controls();
480                if self
481                    .input
482                    .get(self.position)
483                    .is_some_and(u8::is_ascii_digit)
484                {
485                    self.position += 1;
486                } else {
487                    break;
488                }
489            }
490        }
491        self.skip_ignored_controls();
492        if matches!(self.input.get(self.position), Some(b'e' | b'E')) {
493            if !real {
494                return Err(StepError::syntax(
495                    Span::new(start, self.position + 1),
496                    "real requires a decimal point before its exponent",
497                ));
498            }
499            self.position += 1;
500            self.skip_ignored_controls();
501            if matches!(self.input.get(self.position), Some(b'+' | b'-')) {
502                self.position += 1;
503                self.skip_ignored_controls();
504            }
505            let mut exponent_digits = 0;
506            loop {
507                self.skip_ignored_controls();
508                if self
509                    .input
510                    .get(self.position)
511                    .is_some_and(u8::is_ascii_digit)
512                {
513                    exponent_digits += 1;
514                    self.position += 1;
515                } else {
516                    break;
517                }
518            }
519            if exponent_digits == 0 {
520                return Err(StepError::syntax(
521                    Span::new(start, self.position),
522                    "real exponent has no digits",
523                ));
524            }
525        }
526        let text = self.token_bytes(start, self.position);
527        Ok(if real {
528            Token::Real(text)
529        } else {
530            Token::Integer(text)
531        })
532    }
533}
534
535impl<'a> Iterator for Lexer<'a> {
536    type Item = Result<Spanned<Token<'a>>, StepError>;
537
538    fn next(&mut self) -> Option<Self::Item> {
539        match self.next_spanned() {
540            Ok(Some(token)) => Some(Ok(token)),
541            Ok(None) => None,
542            Err(error) => {
543                self.finished = true;
544                Some(Err(error))
545            }
546        }
547    }
548}