Skip to main content

rpic_core/
lexer.rs

1//! Hand-written lexer for the pic language.
2//!
3//! Recognises the full dpic token vocabulary (see `token.rs`). It is a simple
4//! character scanner — pic sources are small, so we keep the whole input in a
5//! `Vec<char>` and index into it. Line/column are tracked for diagnostics.
6
7use crate::token::*;
8
9/// A token together with its source position (1-based line/column).
10#[derive(Debug, Clone, PartialEq)]
11pub struct Spanned {
12    pub tok: Token,
13    pub line: u32,
14    pub col: u32,
15}
16
17/// A lexing error with location.
18#[derive(Debug, Clone, PartialEq)]
19pub struct LexError {
20    pub msg: String,
21    pub line: u32,
22    pub col: u32,
23}
24
25impl std::fmt::Display for LexError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}:{}: {}", self.line, self.col, self.msg)
28    }
29}
30
31/// Tokenize `src`. The returned vector always ends with [`Token::Eof`].
32pub fn lex(src: &str) -> Result<Vec<Spanned>, LexError> {
33    Lexer::new(src).run()
34}
35
36struct Lexer {
37    chars: Vec<char>,
38    pos: usize,
39    line: u32,
40    col: u32,
41    out: Vec<Spanned>,
42}
43
44impl Lexer {
45    fn new(src: &str) -> Self {
46        Lexer {
47            chars: src.chars().collect(),
48            pos: 0,
49            line: 1,
50            col: 1,
51            out: Vec::new(),
52        }
53    }
54
55    fn peek(&self) -> Option<char> {
56        self.chars.get(self.pos).copied()
57    }
58    fn peek_at(&self, n: usize) -> Option<char> {
59        self.chars.get(self.pos + n).copied()
60    }
61
62    /// Consume and return the next char, advancing line/column.
63    fn bump(&mut self) -> Option<char> {
64        let c = self.chars.get(self.pos).copied()?;
65        self.pos += 1;
66        if c == '\n' {
67            self.line += 1;
68            self.col = 1;
69        } else {
70            self.col += 1;
71        }
72        Some(c)
73    }
74
75    fn err<T>(&self, msg: impl Into<String>) -> Result<T, LexError> {
76        Err(LexError {
77            msg: msg.into(),
78            line: self.line,
79            col: self.col,
80        })
81    }
82
83    fn run(mut self) -> Result<Vec<Spanned>, LexError> {
84        loop {
85            // Skip spaces, tabs, CR, and `#` comments. Backslash-newline is a
86            // line continuation (both consumed).
87            loop {
88                match self.peek() {
89                    Some(' ') | Some('\t') | Some('\r') => {
90                        self.bump();
91                    }
92                    Some('#') => {
93                        while let Some(c) = self.peek() {
94                            if c == '\n' {
95                                break;
96                            }
97                            self.bump();
98                        }
99                    }
100                    Some('\\') => {
101                        // continuation: `\` [spaces] newline
102                        let save = (self.pos, self.line, self.col);
103                        self.bump();
104                        while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
105                            self.bump();
106                        }
107                        if self.peek() == Some('\n') {
108                            self.bump();
109                        } else {
110                            // not a continuation; restore and let the main
111                            // matcher report the stray backslash.
112                            self.pos = save.0;
113                            self.line = save.1;
114                            self.col = save.2;
115                            break;
116                        }
117                    }
118                    _ => break,
119                }
120            }
121
122            let (line, col) = (self.line, self.col);
123            let c = match self.peek() {
124                None => {
125                    self.push(Token::Eof, line, col);
126                    return Ok(self.out);
127                }
128                Some(c) => c,
129            };
130
131            let tok = if c == '\n' || c == ';' {
132                self.bump();
133                Token::Newline
134            } else if c == '"' {
135                self.lex_string()?
136            } else if c == '$' {
137                self.lex_arg()?
138            } else if c.is_ascii_digit()
139                || (c == '.' && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()))
140            {
141                self.lex_number()?
142            } else if c == '.' {
143                self.lex_dot()?
144            } else if c.is_alphabetic() || c == '_' {
145                self.lex_word()
146            } else {
147                match self.lex_operator()? {
148                    Some(t) => t,
149                    None => continue, // (should not happen)
150                }
151            };
152            self.push(tok, line, col);
153        }
154    }
155
156    fn push(&mut self, tok: Token, line: u32, col: u32) {
157        self.out.push(Spanned { tok, line, col });
158    }
159
160    fn lex_string(&mut self) -> Result<Token, LexError> {
161        self.bump(); // opening quote
162        let mut s = String::new();
163        loop {
164            match self.bump() {
165                None => return self.err("unterminated string literal"),
166                Some('"') => break,
167                Some('\\') => {
168                    // Preserve escape sequences (e.g. troff escapes) verbatim;
169                    // a backslash-quote does not terminate the string.
170                    s.push('\\');
171                    match self.bump() {
172                        Some(c) => s.push(c),
173                        None => return self.err("unterminated string literal"),
174                    }
175                }
176                Some(c) => s.push(c),
177            }
178        }
179        Ok(Token::Str(s))
180    }
181
182    fn lex_arg(&mut self) -> Result<Token, LexError> {
183        self.bump(); // '$'
184        let mut n = String::new();
185        while let Some(c) = self.peek() {
186            if c.is_ascii_digit() {
187                n.push(c);
188                self.bump();
189            } else {
190                break;
191            }
192        }
193        if n.is_empty() {
194            return self.err("expected digit after `$`");
195        }
196        Ok(Token::Arg(n.parse().unwrap()))
197    }
198
199    fn lex_number(&mut self) -> Result<Token, LexError> {
200        let mut s = String::new();
201        if self.peek() == Some('.') {
202            s.push('0'); // ".5" -> "0.5"
203        }
204        while let Some(c) = self.peek() {
205            if c.is_ascii_digit() {
206                s.push(c);
207                self.bump();
208            } else {
209                break;
210            }
211        }
212        // fractional part: only consume `.` when a digit follows, so a trailing
213        // `.` (e.g. in `box.ne`) stays a separate token.
214        if self.peek() == Some('.') && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()) {
215            s.push('.');
216            self.bump();
217            while let Some(c) = self.peek() {
218                if c.is_ascii_digit() {
219                    s.push(c);
220                    self.bump();
221                } else {
222                    break;
223                }
224            }
225        }
226        // exponent
227        if matches!(self.peek(), Some('e') | Some('E'))
228            && (self.peek_at(1).is_some_and(|d| d.is_ascii_digit())
229                || (matches!(self.peek_at(1), Some('+') | Some('-'))
230                    && self.peek_at(2).is_some_and(|d| d.is_ascii_digit())))
231        {
232            s.push('e');
233            self.bump();
234            if matches!(self.peek(), Some('+') | Some('-')) {
235                s.push(self.bump().unwrap());
236            }
237            while let Some(c) = self.peek() {
238                if c.is_ascii_digit() {
239                    s.push(c);
240                    self.bump();
241                } else {
242                    break;
243                }
244            }
245        }
246        match s.parse::<f64>() {
247            Ok(v) => Ok(Token::Float(v)),
248            Err(_) => self.err(format!("invalid number `{s}`")),
249        }
250    }
251
252    /// Lex a `.`-prefixed token: `.PS/.PE`, compass corners, `.x/.y`, dotted
253    /// attribute accessors, or a bare `.` (the placename separator).
254    fn lex_dot(&mut self) -> Result<Token, LexError> {
255        let save = (self.pos, self.line, self.col);
256        self.bump(); // '.'
257        // read the following word
258        let mut w = String::new();
259        while let Some(c) = self.peek() {
260            if c.is_alphanumeric() || c == '_' {
261                w.push(c);
262                self.bump();
263            } else {
264                break;
265            }
266        }
267        if let Some(tok) = dot_keyword(&w) {
268            Ok(tok)
269        } else {
270            // Not a dotted keyword: emit a bare `.` and rewind so the word is
271            // lexed normally on the next pass.
272            self.pos = save.0 + 1;
273            self.line = save.1;
274            self.col = save.2 + 1;
275            Ok(Token::Dot)
276        }
277    }
278
279    fn lex_word(&mut self) -> Token {
280        let mut w = String::new();
281        while let Some(c) = self.peek() {
282            if c.is_alphanumeric() || c == '_' {
283                w.push(c);
284                self.bump();
285            } else {
286                break;
287            }
288        }
289        word_keyword(&w)
290    }
291
292    fn lex_operator(&mut self) -> Result<Option<Token>, LexError> {
293        let c = self.bump().unwrap();
294        let next = self.peek();
295        let tok = match c {
296            '(' => Token::Lparen,
297            ')' => Token::Rparen,
298            ',' => Token::Comma,
299            '^' => Token::Caret,
300            '{' => Token::LeftBrace,
301            '}' => Token::RightBrace,
302            ']' => Token::RightBrack,
303            '`' => Token::LeftQuote,
304            '\'' => Token::RightQuote,
305            '[' => {
306                if next == Some(']') {
307                    self.bump();
308                    Token::Block
309                } else {
310                    Token::LeftBrack
311                }
312            }
313            ':' => self.two('=', Token::ColonEq, Token::Colon),
314            '=' => self.two('=', Token::EqEq, Token::Eq),
315            '!' => self.two('=', Token::Neq, Token::Not),
316            '+' => self.two('=', Token::PlusEq, Token::Plus),
317            '*' => self.two('=', Token::MultEq, Token::Mult),
318            '/' => self.two('=', Token::DivEq, Token::Div),
319            '%' => self.two('=', Token::RemEq, Token::Percent),
320            '>' => self.two('=', Token::Ge, Token::Gt),
321            '&' => self.two('&', Token::AndAnd, Token::Ampersand),
322            '|' => {
323                if next == Some('|') {
324                    self.bump();
325                    Token::OrOr
326                } else {
327                    return self.err("unexpected `|`");
328                }
329            }
330            '-' => match next {
331                Some('>') => {
332                    self.bump();
333                    Token::Arrow(Arrow::Right)
334                }
335                Some('=') => {
336                    self.bump();
337                    Token::MinusEq
338                }
339                _ => Token::Minus,
340            },
341            '<' => match next {
342                Some('=') => {
343                    self.bump();
344                    Token::Le
345                }
346                Some('-') => {
347                    self.bump();
348                    if self.peek() == Some('>') {
349                        self.bump();
350                        Token::Arrow(Arrow::Double)
351                    } else {
352                        Token::Arrow(Arrow::Left)
353                    }
354                }
355                _ => Token::Lt,
356            },
357            other => return self.err(format!("unexpected character `{other}`")),
358        };
359        Ok(Some(tok))
360    }
361
362    /// If the next char is `c`, consume it and return `yes`; otherwise `no`.
363    fn two(&mut self, c: char, yes: Token, no: Token) -> Token {
364        if self.peek() == Some(c) {
365            self.bump();
366            yes
367        } else {
368            no
369        }
370    }
371}
372
373/// Classify a bare word (after the keyword vocabulary). Unknown words become a
374/// [`Token::Label`] (upper-initial) or [`Token::Name`] (otherwise).
375fn word_keyword(w: &str) -> Token {
376    use Token::*;
377    match w {
378        // primitives
379        "box" => Prim(self::Prim::Box),
380        "circle" => Prim(self::Prim::Circle),
381        "ellipse" => Prim(self::Prim::Ellipse),
382        "arc" => Prim(self::Prim::Arc),
383        "line" => Prim(self::Prim::Line),
384        "arrow" => Prim(self::Prim::Arrow),
385        "move" => Prim(self::Prim::Move),
386        "spline" => Prim(self::Prim::Spline),
387        // directions
388        "up" => Dir(self::Dir::Up),
389        "down" => Dir(self::Dir::Down),
390        "right" => Dir(self::Dir::Right),
391        "left" => Dir(self::Dir::Left),
392        // attributes & joiners
393        "height" | "ht" => Kw(self::Kw::Ht),
394        "width" | "wid" => Kw(self::Kw::Wid),
395        "radius" | "rad" => Kw(self::Kw::Rad),
396        "diameter" | "diam" => Kw(self::Kw::Diam),
397        "thickness" | "thick" => Kw(self::Kw::Thick),
398        "scaled" => Kw(self::Kw::Scaled),
399        "from" => Kw(self::Kw::From),
400        "to" => Kw(self::Kw::To),
401        "at" => Kw(self::Kw::At),
402        "with" => Kw(self::Kw::With),
403        "by" => Kw(self::Kw::By),
404        "then" => Kw(self::Kw::Then),
405        "cw" => Kw(self::Kw::Cw),
406        "ccw" => Kw(self::Kw::Ccw),
407        "continue" => Kw(self::Kw::Continue),
408        "chop" => Kw(self::Kw::Chop),
409        "same" => Kw(self::Kw::Same),
410        "of" => Kw(self::Kw::Of),
411        "the" => Kw(self::Kw::The),
412        "way" => Kw(self::Kw::Way),
413        "between" => Kw(self::Kw::Between),
414        "and" => Kw(self::Kw::And),
415        "last" => Kw(self::Kw::Last),
416        "fill" | "filled" => Kw(self::Kw::Fill),
417        "st" | "nd" | "rd" | "th" => Kw(self::Kw::Nth),
418        "Here" => Kw(self::Kw::Here),
419        // bare corner words
420        "top" => Corner(self::Corner::N),
421        "bottom" => Corner(self::Corner::S),
422        "start" => Corner(self::Corner::Start),
423        "end" => Corner(self::Corner::End),
424        // commands / control
425        "print" => Kw(self::Kw::Print),
426        "copy" => Kw(self::Kw::Copy),
427        "reset" => Kw(self::Kw::Reset),
428        "exec" => Kw(self::Kw::Exec),
429        "sh" => Kw(self::Kw::Sh),
430        "command" => Kw(self::Kw::Command),
431        "define" => Kw(self::Kw::Define),
432        "undefine" | "undef" => Kw(self::Kw::Undef),
433        "rand" => Kw(self::Kw::Rand),
434        "if" => Kw(self::Kw::If),
435        "else" => Kw(self::Kw::Else),
436        "for" => Kw(self::Kw::For),
437        "do" => Kw(self::Kw::Do),
438        "sprintf" => Kw(self::Kw::Sprintf),
439        // rpic animation extension
440        "animate" => Kw(self::Kw::Animate),
441        "after" => Kw(self::Kw::After),
442        "delay" => Kw(self::Kw::Delay),
443        // line types
444        "solid" => LineType(self::LineType::Solid),
445        "dotted" => LineType(self::LineType::Dotted),
446        "dashed" => LineType(self::LineType::Dashed),
447        "invis" | "invisible" => LineType(self::LineType::Invis),
448        // color / outline / shade
449        "color" | "colour" | "colored" | "coloured" => Color(self::Color::Colored),
450        "outline" | "outlined" => Color(self::Color::Outlined),
451        "shade" | "shaded" => Color(self::Color::Shaded),
452        // text position
453        "center" | "centre" => TextPos(self::TextPos::Center),
454        "ljust" => TextPos(self::TextPos::Ljust),
455        "rjust" => TextPos(self::TextPos::Rjust),
456        "above" => TextPos(self::TextPos::Above),
457        "below" => TextPos(self::TextPos::Below),
458        // one-arg functions
459        "abs" => Func1(self::Func1::Abs),
460        "acos" => Func1(self::Func1::Acos),
461        "asin" => Func1(self::Func1::Asin),
462        "cos" => Func1(self::Func1::Cos),
463        "exp" => Func1(self::Func1::Exp),
464        "expe" => Func1(self::Func1::Expe),
465        "int" => Func1(self::Func1::Int),
466        "log" => Func1(self::Func1::Log),
467        "loge" => Func1(self::Func1::Loge),
468        "sign" => Func1(self::Func1::Sign),
469        "sin" => Func1(self::Func1::Sin),
470        "sqrt" => Func1(self::Func1::Sqrt),
471        "tan" => Func1(self::Func1::Tan),
472        "floor" => Func1(self::Func1::Floor),
473        // two-arg functions
474        "atan2" => Func2(self::Func2::Atan2),
475        "max" => Func2(self::Func2::Max),
476        "min" => Func2(self::Func2::Min),
477        "pmod" => Func2(self::Func2::Pmod),
478        // environment variables
479        "arcrad" => EnvVar(self::EnvVar::Arcrad),
480        "arrowht" => EnvVar(self::EnvVar::Arrowht),
481        "arrowwid" => EnvVar(self::EnvVar::Arrowwid),
482        "boxht" => EnvVar(self::EnvVar::Boxht),
483        "boxrad" => EnvVar(self::EnvVar::Boxrad),
484        "boxwid" => EnvVar(self::EnvVar::Boxwid),
485        "circlerad" => EnvVar(self::EnvVar::Circlerad),
486        "dashwid" => EnvVar(self::EnvVar::Dashwid),
487        "ellipseht" => EnvVar(self::EnvVar::Ellipseht),
488        "ellipsewid" => EnvVar(self::EnvVar::Ellipsewid),
489        "lineht" => EnvVar(self::EnvVar::Lineht),
490        "linewid" => EnvVar(self::EnvVar::Linewid),
491        "moveht" => EnvVar(self::EnvVar::Moveht),
492        "movewid" => EnvVar(self::EnvVar::Movewid),
493        "textht" => EnvVar(self::EnvVar::Textht),
494        "textoffset" => EnvVar(self::EnvVar::Textoffset),
495        "textwid" => EnvVar(self::EnvVar::Textwid),
496        "arrowhead" => EnvVar(self::EnvVar::Arrowhead),
497        "fillval" => EnvVar(self::EnvVar::Fillval),
498        "linethick" => EnvVar(self::EnvVar::Linethick),
499        "maxpsht" => EnvVar(self::EnvVar::Maxpsht),
500        "maxpswid" => EnvVar(self::EnvVar::Maxpswid),
501        "scale" => EnvVar(self::EnvVar::Scale),
502        // identifier / label
503        _ => {
504            if w.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
505                Label(w.to_string())
506            } else {
507                Name(w.to_string())
508            }
509        }
510    }
511}
512
513/// Classify the word following a `.`. Returns `None` if it is not a dotted
514/// keyword (so the caller emits a bare `.`).
515fn dot_keyword(w: &str) -> Option<Token> {
516    use Token::*;
517    let t = match w {
518        "PS" => DotPS,
519        "PE" => DotPE,
520        "x" => DotX,
521        "y" => DotY,
522        // compass corners
523        "ne" => Corner(self::Corner::Ne),
524        "se" => Corner(self::Corner::Se),
525        "nw" => Corner(self::Corner::Nw),
526        "sw" => Corner(self::Corner::Sw),
527        "n" | "t" | "top" | "north" => Corner(self::Corner::N),
528        "s" | "b" | "bot" | "bottom" | "south" => Corner(self::Corner::S),
529        "e" | "r" | "east" | "right" => Corner(self::Corner::E),
530        "w" | "l" | "west" | "left" => Corner(self::Corner::W),
531        "start" => Corner(self::Corner::Start),
532        "end" => Corner(self::Corner::End),
533        "c" | "center" | "centre" => Corner(self::Corner::Center),
534        // dotted attribute accessors
535        "ht" | "height" => Param(self::Param::Height),
536        "wid" | "width" => Param(self::Param::Width),
537        "rad" | "radius" => Param(self::Param::Radius),
538        "diam" | "diameter" => Param(self::Param::Diameter),
539        "thick" | "thickness" => Param(self::Param::Thickness),
540        "len" | "length" => Param(self::Param::Length),
541        _ => return None,
542    };
543    Some(t)
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    fn toks(src: &str) -> Vec<Token> {
551        lex(src).unwrap().into_iter().map(|s| s.tok).collect()
552    }
553
554    #[test]
555    fn basic_box() {
556        assert_eq!(
557            toks("box \"hi\""),
558            vec![Token::Prim(Prim::Box), Token::Str("hi".into()), Token::Eof]
559        );
560    }
561
562    #[test]
563    fn label_vs_name() {
564        assert_eq!(
565            toks("Start: boxwid"),
566            vec![
567                Token::Label("Start".into()),
568                Token::Colon,
569                Token::EnvVar(EnvVar::Boxwid),
570                Token::Eof
571            ]
572        );
573        // a lower-initial non-keyword is a Name
574        assert_eq!(toks("myvar"), vec![Token::Name("myvar".into()), Token::Eof]);
575    }
576
577    #[test]
578    fn numbers() {
579        assert_eq!(toks("0.5"), vec![Token::Float(0.5), Token::Eof]);
580        assert_eq!(toks(".25"), vec![Token::Float(0.25), Token::Eof]);
581        assert_eq!(toks("1e3"), vec![Token::Float(1000.0), Token::Eof]);
582        assert_eq!(toks("2.5e-1"), vec![Token::Float(0.25), Token::Eof]);
583    }
584
585    #[test]
586    fn operators_and_arrows() {
587        assert_eq!(
588            toks("a := b <= c -> d <- e <-> f"),
589            vec![
590                Token::Name("a".into()),
591                Token::ColonEq,
592                Token::Name("b".into()),
593                Token::Le,
594                Token::Name("c".into()),
595                Token::Arrow(Arrow::Right),
596                Token::Name("d".into()),
597                Token::Arrow(Arrow::Left),
598                Token::Name("e".into()),
599                Token::Arrow(Arrow::Double),
600                Token::Name("f".into()),
601                Token::Eof
602            ]
603        );
604    }
605
606    #[test]
607    fn minus_is_not_arrow_without_gt() {
608        assert_eq!(
609            toks("2-3"),
610            vec![
611                Token::Float(2.0),
612                Token::Minus,
613                Token::Float(3.0),
614                Token::Eof
615            ]
616        );
617    }
618
619    #[test]
620    fn compass_and_params() {
621        assert_eq!(
622            toks("last box.ne A.ht .center"),
623            vec![
624                Token::Kw(Kw::Last),
625                Token::Prim(Prim::Box),
626                Token::Corner(Corner::Ne),
627                Token::Label("A".into()),
628                Token::Param(Param::Height),
629                Token::Corner(Corner::Center),
630                Token::Eof
631            ]
632        );
633    }
634
635    #[test]
636    fn dot_then_label() {
637        // `.` not followed by a dotted keyword is a bare Dot, then the word.
638        assert_eq!(
639            toks("B.A"),
640            vec![
641                Token::Label("B".into()),
642                Token::Dot,
643                Token::Label("A".into()),
644                Token::Eof
645            ]
646        );
647    }
648
649    #[test]
650    fn statement_separators_and_comments() {
651        assert_eq!(
652            toks("box; circle # a comment\narc"),
653            vec![
654                Token::Prim(Prim::Box),
655                Token::Newline,
656                Token::Prim(Prim::Circle),
657                Token::Newline,
658                Token::Prim(Prim::Arc),
659                Token::Eof
660            ]
661        );
662    }
663
664    #[test]
665    fn line_continuation() {
666        assert_eq!(
667            toks("box \\\n  wid 2"),
668            vec![
669                Token::Prim(Prim::Box),
670                Token::Kw(Kw::Wid),
671                Token::Float(2.0),
672                Token::Eof
673            ]
674        );
675    }
676
677    #[test]
678    fn ps_pe_and_block() {
679        assert_eq!(
680            toks(".PS\nbox\n.PE"),
681            vec![
682                Token::DotPS,
683                Token::Newline,
684                Token::Prim(Prim::Box),
685                Token::Newline,
686                Token::DotPE,
687                Token::Eof
688            ]
689        );
690        assert_eq!(toks("[]"), vec![Token::Block, Token::Eof]);
691        assert_eq!(
692            toks("[ box ]"),
693            vec![
694                Token::LeftBrack,
695                Token::Prim(Prim::Box),
696                Token::RightBrack,
697                Token::Eof
698            ]
699        );
700    }
701
702    #[test]
703    fn macro_arg() {
704        assert_eq!(
705            toks("box $1"),
706            vec![Token::Prim(Prim::Box), Token::Arg(1), Token::Eof]
707        );
708    }
709
710    #[test]
711    fn func_and_envvar() {
712        assert_eq!(
713            toks("sqrt(2) atan2 scale"),
714            vec![
715                Token::Func1(Func1::Sqrt),
716                Token::Lparen,
717                Token::Float(2.0),
718                Token::Rparen,
719                Token::Func2(Func2::Atan2),
720                Token::EnvVar(EnvVar::Scale),
721                Token::Eof
722            ]
723        );
724    }
725}