1use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::diagnostic::Span;
11use crate::token::*;
12
13#[derive(Clone)]
15pub struct Spanned {
16 pub tok: Token,
17 pub line: u32,
18 pub col: u32,
19 pub end_col: u32,
20 pub file: Option<Arc<str>>,
25 pub arg_frame: Option<Arc<Vec<Vec<Spanned>>>>,
28 pub macro_frame: Option<Arc<HashMap<String, Vec<Spanned>>>>,
31}
32
33impl Spanned {
34 pub fn new(tok: Token, line: u32, col: u32) -> Self {
35 Self {
36 tok,
37 line,
38 col,
39 end_col: col.saturating_add(1),
40 file: None,
41 arg_frame: None,
42 macro_frame: None,
43 }
44 }
45
46 pub fn with_end(mut self, end_col: u32) -> Self {
47 self.end_col = end_col;
48 self
49 }
50
51 pub fn with_file(mut self, file: Option<Arc<str>>) -> Self {
52 self.file = file;
53 self
54 }
55
56 pub fn span(&self) -> Span {
57 Span::new(self.line, self.col, self.end_col).in_file(self.file.clone())
58 }
59
60 pub fn with_arg_frame(mut self, args: &Arc<Vec<Vec<Spanned>>>) -> Self {
61 self.arg_frame = Some(Arc::clone(args));
62 self
63 }
64
65 pub fn with_macro_frame(mut self, macros: &HashMap<String, Vec<Spanned>>) -> Self {
66 self.macro_frame = Some(Arc::new(macros.clone()));
67 self
68 }
69}
70
71impl std::fmt::Debug for Spanned {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("Spanned")
74 .field("tok", &self.tok)
75 .field("line", &self.line)
76 .field("col", &self.col)
77 .field("end_col", &self.end_col)
78 .field("arg_frame", &self.arg_frame)
79 .finish_non_exhaustive()
80 }
81}
82
83impl PartialEq for Spanned {
84 fn eq(&self, other: &Self) -> bool {
85 self.tok == other.tok
86 && self.line == other.line
87 && self.col == other.col
88 && self.end_col == other.end_col
89 && self.arg_frame == other.arg_frame
90 }
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct LexError {
96 pub msg: String,
97 pub line: u32,
98 pub col: u32,
99 pub end_col: u32,
100 pub file: Option<Arc<str>>,
102 pub kind: String,
103}
104
105impl std::fmt::Display for LexError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 match &self.file {
108 Some(file) => write!(f, "{}:{}:{}: {}", file, self.line, self.col, self.msg),
109 None => write!(f, "{}:{}: {}", self.line, self.col, self.msg),
110 }
111 }
112}
113
114pub fn lex(src: &str) -> Result<Vec<Spanned>, LexError> {
116 Lexer::new(src, None).run()
117}
118
119pub fn lex_named(src: &str, file: &str) -> Result<Vec<Spanned>, LexError> {
124 Lexer::new(src, Some(Arc::from(file))).run()
125}
126
127struct Lexer {
128 chars: Vec<char>,
129 pos: usize,
130 line: u32,
131 col: u32,
132 file: Option<Arc<str>>,
133 out: Vec<Spanned>,
134}
135
136impl Lexer {
137 fn new(src: &str, file: Option<Arc<str>>) -> Self {
138 Lexer {
139 chars: src.chars().collect(),
140 pos: 0,
141 line: 1,
142 col: 1,
143 file,
144 out: Vec::new(),
145 }
146 }
147
148 fn peek(&self) -> Option<char> {
149 self.chars.get(self.pos).copied()
150 }
151 fn peek_at(&self, n: usize) -> Option<char> {
152 self.chars.get(self.pos + n).copied()
153 }
154
155 fn bump(&mut self) -> Option<char> {
157 let c = self.chars.get(self.pos).copied()?;
158 self.pos += 1;
159 if c == '\n' {
160 self.line += 1;
161 self.col = 1;
162 } else {
163 self.col += 1;
164 }
165 Some(c)
166 }
167
168 fn err<T>(&self, msg: impl Into<String>) -> Result<T, LexError> {
169 self.err_at(self.line, self.col, self.col.saturating_add(1), "lex", msg)
170 }
171
172 fn err_at<T>(
173 &self,
174 line: u32,
175 col: u32,
176 end_col: u32,
177 kind: impl Into<String>,
178 msg: impl Into<String>,
179 ) -> Result<T, LexError> {
180 Err(LexError {
181 msg: msg.into(),
182 line,
183 col,
184 end_col,
185 file: self.file.clone(),
186 kind: kind.into(),
187 })
188 }
189
190 fn run(mut self) -> Result<Vec<Spanned>, LexError> {
191 loop {
192 loop {
195 match self.peek() {
196 Some(' ') | Some('\t') | Some('\r') => {
197 self.bump();
198 }
199 Some('#') => {
200 while let Some(c) = self.peek() {
201 if c == '\n' {
202 break;
203 }
204 self.bump();
205 }
206 }
207 Some('\\') => {
208 let save = (self.pos, self.line, self.col);
210 self.bump();
211 while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
212 self.bump();
213 }
214 if self.peek() == Some('\n') {
215 self.bump();
216 } else {
217 self.pos = save.0;
220 self.line = save.1;
221 self.col = save.2;
222 break;
223 }
224 }
225 _ => break,
226 }
227 }
228
229 let (line, col) = (self.line, self.col);
230 let c = match self.peek() {
231 None => {
232 self.push(Token::Eof, line, col, col);
233 return Ok(self.out);
234 }
235 Some(c) => c,
236 };
237
238 let tok = if c == '\n' || c == ';' {
239 self.bump();
240 Token::Newline
241 } else if c == '"' {
242 self.lex_string()?
243 } else if c == '$' {
244 self.lex_arg()?
245 } else if c.is_ascii_digit()
246 || (c == '.' && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()))
247 {
248 self.lex_number()?
249 } else if c == '.' {
250 self.lex_dot()?
251 } else if c.is_alphabetic() || c == '_' {
252 let tok = self.lex_word();
253 if matches!(tok, Token::Kw(Kw::Sh | Kw::Command)) {
254 self.skip_raw_command_arg();
255 }
256 tok
257 } else {
258 match self.lex_operator()? {
259 Some(t) => t,
260 None => continue, }
262 };
263 let end_col = if self.line == line {
264 self.col
265 } else {
266 col.saturating_add(1)
267 };
268 self.push(tok, line, col, end_col);
269 }
270 }
271
272 fn push(&mut self, tok: Token, line: u32, col: u32, end_col: u32) {
273 if tok == Token::Newline
280 && matches!(self.out.last().map(|s| &s.tok), Some(Token::Kw(Kw::Then)))
281 {
282 return;
283 }
284 if tok == Token::Kw(Kw::Then)
285 && matches!(self.out.last().map(|s| &s.tok), Some(Token::Newline))
286 {
287 self.out.pop();
288 }
289 self.out.push(
290 Spanned::new(tok, line, col)
291 .with_end(end_col)
292 .with_file(self.file.clone()),
293 );
294 }
295
296 fn lex_string(&mut self) -> Result<Token, LexError> {
297 let (start_line, start_col) = (self.line, self.col);
298 self.bump(); let mut s = String::new();
300 loop {
301 match self.bump() {
302 None => {
303 return self.err_at(
304 start_line,
305 start_col,
306 start_col.saturating_add(1),
307 "unterminated_string",
308 "unterminated string literal",
309 );
310 }
311 Some('"') => break,
312 Some('\\') => {
313 s.push('\\');
316 match self.bump() {
317 Some(c) => s.push(c),
318 None => {
319 return self.err_at(
320 start_line,
321 start_col,
322 start_col.saturating_add(1),
323 "unterminated_string",
324 "unterminated string literal",
325 );
326 }
327 }
328 }
329 Some(c) => s.push(c),
330 }
331 }
332 Ok(Token::Str(s))
333 }
334
335 fn skip_raw_command_arg(&mut self) {
336 while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
337 self.bump();
338 }
339 if self.peek() == Some('"') {
340 self.skip_raw_quoted();
341 } else if self.starts_word_here("sprintf") {
342 for _ in 0.."sprintf".len() {
343 self.bump();
344 }
345 while matches!(self.peek(), Some(' ') | Some('\t') | Some('\r')) {
346 self.bump();
347 }
348 if self.peek() == Some('(') {
349 self.skip_raw_parens();
350 }
351 } else {
352 self.skip_raw_line_tail();
353 }
354 }
355
356 fn starts_word_here(&self, word: &str) -> bool {
357 for (i, want) in word.chars().enumerate() {
358 if self.peek_at(i) != Some(want) {
359 return false;
360 }
361 }
362 !self
363 .peek_at(word.len())
364 .is_some_and(|c| c.is_alphanumeric() || c == '_')
365 }
366
367 fn skip_raw_quoted(&mut self) {
368 if self.peek() != Some('"') {
369 return;
370 }
371 self.bump();
372 while let Some(c) = self.bump() {
373 match c {
374 '\\' => {
375 self.bump();
376 }
377 '"' if self.raw_quote_is_followed_by(&['\n', ';', '}']) => break,
378 _ => {}
379 }
380 }
381 }
382
383 fn skip_raw_parens(&mut self) {
384 let mut depth = 0i32;
385 while let Some(c) = self.bump() {
386 match c {
387 '"' => self.skip_raw_quoted_tail(),
388 '(' => depth += 1,
389 ')' => {
390 depth -= 1;
391 if depth == 0 {
392 break;
393 }
394 }
395 _ => {}
396 }
397 }
398 }
399
400 fn skip_raw_quoted_tail(&mut self) {
401 while let Some(c) = self.bump() {
402 match c {
403 '\\' => {
404 self.bump();
405 }
406 '"' if self.raw_quote_is_followed_by(&['\n', ';', '}', ',', ')']) => break,
407 _ => {}
408 }
409 }
410 }
411
412 fn raw_quote_is_followed_by(&self, stops: &[char]) -> bool {
413 let mut off = 0;
414 loop {
415 match self.peek_at(off) {
416 Some(' ' | '\t' | '\r') => off += 1,
417 Some(c) => return stops.contains(&c),
418 None => return true,
419 }
420 }
421 }
422
423 fn skip_raw_line_tail(&mut self) {
424 loop {
425 let mut last_non_ws = None;
426 while let Some(c) = self.peek() {
427 if c == '\n' {
428 break;
429 }
430 if !matches!(c, ' ' | '\t' | '\r') {
431 last_non_ws = Some(c);
432 }
433 self.bump();
434 }
435 if self.peek() == Some('\n') && last_non_ws == Some('\\') {
436 self.bump();
437 continue;
438 }
439 break;
440 }
441 }
442
443 fn lex_arg(&mut self) -> Result<Token, LexError> {
444 self.bump(); if self.peek() == Some('+') {
447 self.bump();
448 return Ok(Token::ArgCount);
449 }
450 let mut n = String::new();
451 while let Some(c) = self.peek() {
452 if c.is_ascii_digit() {
453 n.push(c);
454 self.bump();
455 } else {
456 break;
457 }
458 }
459 if n.is_empty() {
460 return Ok(Token::Dollar);
463 }
464 match n.parse() {
465 Ok(v) => Ok(Token::Arg(v)),
466 Err(_) => self.err(format!("macro argument `${n}` is too large")),
467 }
468 }
469
470 fn lex_number(&mut self) -> Result<Token, LexError> {
471 if self.peek() == Some('0')
475 && matches!(self.peek_at(1), Some('x') | Some('X'))
476 && self.peek_at(2).is_some_and(|d| d.is_ascii_hexdigit())
477 {
478 self.bump();
479 self.bump();
480 let mut v: f64 = 0.0;
481 while let Some(c) = self.peek() {
482 if let Some(d) = c.to_digit(16) {
483 v = v * 16.0 + d as f64;
484 self.bump();
485 } else {
486 break;
487 }
488 }
489 return Ok(Token::Float(v));
490 }
491 let mut s = String::new();
492 if self.peek() == Some('.') {
493 s.push('0'); }
495 while let Some(c) = self.peek() {
496 if c.is_ascii_digit() {
497 s.push(c);
498 self.bump();
499 } else {
500 break;
501 }
502 }
503 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()) {
506 s.push('.');
507 self.bump();
508 while let Some(c) = self.peek() {
509 if c.is_ascii_digit() {
510 s.push(c);
511 self.bump();
512 } else {
513 break;
514 }
515 }
516 }
517 if matches!(self.peek(), Some('e') | Some('E'))
519 && (self.peek_at(1).is_some_and(|d| d.is_ascii_digit())
520 || (matches!(self.peek_at(1), Some('+') | Some('-'))
521 && self.peek_at(2).is_some_and(|d| d.is_ascii_digit())))
522 {
523 s.push('e');
524 self.bump();
525 if matches!(self.peek(), Some('+') | Some('-')) {
526 s.push(self.bump().unwrap());
527 }
528 while let Some(c) = self.peek() {
529 if c.is_ascii_digit() {
530 s.push(c);
531 self.bump();
532 } else {
533 break;
534 }
535 }
536 }
537 if matches!(self.peek(), Some('i') | Some('I'))
541 && !self
542 .peek_at(1)
543 .is_some_and(|c| c.is_alphanumeric() || c == '_')
544 {
545 self.bump();
546 }
547 match s.parse::<f64>() {
548 Ok(v) if v.is_finite() => Ok(Token::Float(v)),
549 Ok(_) => self.err(format!("number `{s}` is not finite")),
550 Err(_) => self.err(format!("invalid number `{s}`")),
551 }
552 }
553
554 fn lex_dot(&mut self) -> Result<Token, LexError> {
557 let save = (self.pos, self.line, self.col);
558 self.bump(); let mut w = String::new();
561 while let Some(c) = self.peek() {
562 if c.is_alphanumeric() || c == '_' {
563 w.push(c);
564 self.bump();
565 } else {
566 break;
567 }
568 }
569 if let Some(tok) = dot_keyword(&w) {
570 Ok(tok)
571 } else {
572 self.pos = save.0 + 1;
575 self.line = save.1;
576 self.col = save.2 + 1;
577 Ok(Token::Dot)
578 }
579 }
580
581 fn lex_word(&mut self) -> Token {
582 let mut w = String::new();
583 while let Some(c) = self.peek() {
584 if c.is_alphanumeric() || c == '_' {
585 w.push(c);
586 self.bump();
587 } else {
588 break;
589 }
590 }
591 word_keyword(&w)
592 }
593
594 fn lex_operator(&mut self) -> Result<Option<Token>, LexError> {
595 let c = self.bump().unwrap();
596 let next = self.peek();
597 let tok = match c {
598 '(' => Token::Lparen,
599 ')' => Token::Rparen,
600 ',' => Token::Comma,
601 '^' => Token::Caret,
602 '{' => Token::LeftBrace,
603 '}' => Token::RightBrace,
604 ']' => Token::RightBrack,
605 '`' => Token::LeftQuote,
606 '\'' => Token::RightQuote,
607 '[' => {
608 if next == Some(']') {
609 self.bump();
610 Token::Block
611 } else {
612 Token::LeftBrack
613 }
614 }
615 ':' => self.two('=', Token::ColonEq, Token::Colon),
616 '=' => self.two('=', Token::EqEq, Token::Eq),
617 '!' => self.two('=', Token::Neq, Token::Not),
618 '+' => self.two('=', Token::PlusEq, Token::Plus),
619 '*' => self.two('=', Token::MultEq, Token::Mult),
620 '/' => self.two('=', Token::DivEq, Token::Div),
621 '%' => self.two('=', Token::RemEq, Token::Percent),
622 '>' => self.two('=', Token::Ge, Token::Gt),
623 '&' => self.two('&', Token::AndAnd, Token::Ampersand),
624 '|' => {
625 if next == Some('|') {
626 self.bump();
627 Token::OrOr
628 } else {
629 return self.err("unexpected `|`");
630 }
631 }
632 '-' => match next {
633 Some('>') => {
634 self.bump();
635 Token::Arrow(Arrow::Right)
636 }
637 Some('=') => {
638 self.bump();
639 Token::MinusEq
640 }
641 _ => Token::Minus,
642 },
643 '<' => match next {
644 Some('=') => {
645 self.bump();
646 Token::Le
647 }
648 Some('-') => {
649 self.bump();
650 if self.peek() == Some('>') {
651 self.bump();
652 Token::Arrow(Arrow::Double)
653 } else {
654 Token::Arrow(Arrow::Left)
655 }
656 }
657 _ => Token::Lt,
658 },
659 '\\' => Token::Backslash,
664 other => return self.err(format!("unexpected character `{other}`")),
665 };
666 Ok(Some(tok))
667 }
668
669 fn two(&mut self, c: char, yes: Token, no: Token) -> Token {
671 if self.peek() == Some(c) {
672 self.bump();
673 yes
674 } else {
675 no
676 }
677 }
678}
679
680fn word_keyword(w: &str) -> Token {
683 use Token::*;
684 match w {
685 "box" => Prim(self::Prim::Box),
687 "circle" => Prim(self::Prim::Circle),
688 "ellipse" => Prim(self::Prim::Ellipse),
689 "arc" => Prim(self::Prim::Arc),
690 "line" => Prim(self::Prim::Line),
691 "arrow" => Prim(self::Prim::Arrow),
692 "move" => Prim(self::Prim::Move),
693 "spline" => Prim(self::Prim::Spline),
694 "up" => Dir(self::Dir::Up),
696 "down" => Dir(self::Dir::Down),
697 "right" => Dir(self::Dir::Right),
698 "left" => Dir(self::Dir::Left),
699 "height" | "ht" => Kw(self::Kw::Ht),
701 "width" | "wid" => Kw(self::Kw::Wid),
702 "radius" | "rad" => Kw(self::Kw::Rad),
703 "diameter" | "diam" => Kw(self::Kw::Diam),
704 "thickness" | "thick" => Kw(self::Kw::Thick),
705 "thin" => Kw(self::Kw::Thin),
706 "scaled" => Kw(self::Kw::Scaled),
707 "from" => Kw(self::Kw::From),
708 "to" => Kw(self::Kw::To),
709 "at" => Kw(self::Kw::At),
710 "with" => Kw(self::Kw::With),
711 "by" => Kw(self::Kw::By),
712 "then" => Kw(self::Kw::Then),
713 "cw" => Kw(self::Kw::Cw),
714 "ccw" => Kw(self::Kw::Ccw),
715 "continue" => Kw(self::Kw::Continue),
716 "chop" => Kw(self::Kw::Chop),
717 "same" => Kw(self::Kw::Same),
718 "of" => Kw(self::Kw::Of),
719 "the" => Kw(self::Kw::The),
720 "way" => Kw(self::Kw::Way),
721 "between" => Kw(self::Kw::Between),
722 "and" => Kw(self::Kw::And),
723 "last" | "previous" => Kw(self::Kw::Last),
727 "fill" | "filled" => Kw(self::Kw::Fill),
728 "st" | "nd" | "rd" | "th" => Kw(self::Kw::Nth),
729 "Here" => Kw(self::Kw::Here),
730 "top" => Corner(self::Corner::N),
732 "bottom" => Corner(self::Corner::S),
733 "start" => Corner(self::Corner::Start),
734 "end" => Corner(self::Corner::End),
735 "print" => Kw(self::Kw::Print),
737 "copy" => Kw(self::Kw::Copy),
738 "reset" => Kw(self::Kw::Reset),
739 "exec" => Kw(self::Kw::Exec),
740 "sh" => Kw(self::Kw::Sh),
741 "command" => Kw(self::Kw::Command),
742 "define" => Kw(self::Kw::Define),
743 "undefine" | "undef" => Kw(self::Kw::Undef),
744 "rand" => Kw(self::Kw::Rand),
745 "if" => Kw(self::Kw::If),
746 "else" => Kw(self::Kw::Else),
747 "for" => Kw(self::Kw::For),
748 "do" => Kw(self::Kw::Do),
749 "sprintf" => Kw(self::Kw::Sprintf),
750 "animate" => Kw(self::Kw::Animate),
752 "after" => Kw(self::Kw::After),
753 "delay" => Kw(self::Kw::Delay),
754 "repeat" => Kw(self::Kw::Repeat),
755 "yoyo" => Kw(self::Kw::Yoyo),
756 "ease" => Kw(self::Kw::Ease),
757 "along" => Kw(self::Kw::Along),
758 "stagger" => Kw(self::Kw::Stagger),
759 "out" => Kw(self::Kw::Out),
760 "scroll" => Kw(self::Kw::Scroll),
761 "into" => Kw(self::Kw::Into),
762 "solid" => LineType(self::LineType::Solid),
764 "dotted" => LineType(self::LineType::Dotted),
765 "dashed" => LineType(self::LineType::Dashed),
766 "invis" | "invisible" => LineType(self::LineType::Invis),
767 "color" | "colour" | "colored" | "coloured" => Color(self::Color::Colored),
769 "outline" | "outlined" => Color(self::Color::Outlined),
770 "shade" | "shaded" => Color(self::Color::Shaded),
771 "center" | "centre" => TextPos(self::TextPos::Center),
773 "ljust" => TextPos(self::TextPos::Ljust),
774 "rjust" => TextPos(self::TextPos::Rjust),
775 "above" => TextPos(self::TextPos::Above),
776 "below" => TextPos(self::TextPos::Below),
777 "abs" => Func1(self::Func1::Abs),
779 "acos" => Func1(self::Func1::Acos),
780 "asin" => Func1(self::Func1::Asin),
781 "cos" => Func1(self::Func1::Cos),
782 "exp" => Func1(self::Func1::Exp),
783 "expe" => Func1(self::Func1::Expe),
784 "int" => Func1(self::Func1::Int),
785 "log" => Func1(self::Func1::Log),
786 "loge" => Func1(self::Func1::Loge),
787 "sign" => Func1(self::Func1::Sign),
788 "sin" => Func1(self::Func1::Sin),
789 "sqrt" => Func1(self::Func1::Sqrt),
790 "tan" => Func1(self::Func1::Tan),
791 "floor" => Func1(self::Func1::Floor),
792 "atan2" => Func2(self::Func2::Atan2),
794 "max" => Func2(self::Func2::Max),
795 "min" => Func2(self::Func2::Min),
796 "pmod" => Func2(self::Func2::Pmod),
797 "arcrad" => EnvVar(self::EnvVar::Arcrad),
799 "arrowht" => EnvVar(self::EnvVar::Arrowht),
800 "arrowwid" => EnvVar(self::EnvVar::Arrowwid),
801 "boxht" => EnvVar(self::EnvVar::Boxht),
802 "boxrad" => EnvVar(self::EnvVar::Boxrad),
803 "boxwid" => EnvVar(self::EnvVar::Boxwid),
804 "circlerad" => EnvVar(self::EnvVar::Circlerad),
805 "dashwid" => EnvVar(self::EnvVar::Dashwid),
806 "ellipseht" => EnvVar(self::EnvVar::Ellipseht),
807 "ellipsewid" => EnvVar(self::EnvVar::Ellipsewid),
808 "lineht" => EnvVar(self::EnvVar::Lineht),
809 "linewid" => EnvVar(self::EnvVar::Linewid),
810 "moveht" => EnvVar(self::EnvVar::Moveht),
811 "movewid" => EnvVar(self::EnvVar::Movewid),
812 "textht" => EnvVar(self::EnvVar::Textht),
813 "textoffset" => EnvVar(self::EnvVar::Textoffset),
814 "textwid" => EnvVar(self::EnvVar::Textwid),
815 "arrowhead" => EnvVar(self::EnvVar::Arrowhead),
816 "fillval" => EnvVar(self::EnvVar::Fillval),
817 "linethick" => EnvVar(self::EnvVar::Linethick),
818 "margin" => EnvVar(self::EnvVar::Margin),
819 "topmargin" => EnvVar(self::EnvVar::Topmargin),
820 "rightmargin" => EnvVar(self::EnvVar::Rightmargin),
821 "bottommargin" => EnvVar(self::EnvVar::Bottommargin),
822 "leftmargin" => EnvVar(self::EnvVar::Leftmargin),
823 "maxpsht" => EnvVar(self::EnvVar::Maxpsht),
824 "maxpswid" => EnvVar(self::EnvVar::Maxpswid),
825 "scale" => EnvVar(self::EnvVar::Scale),
826 "texlabels" => EnvVar(self::EnvVar::Texlabels),
827 "dotrad" => EnvVar(self::EnvVar::Dotrad),
828 "maxanimrepeat" => EnvVar(self::EnvVar::Maxanimrepeat),
829 "maxrepeats" => EnvVar(self::EnvVar::Maxanimrepeat),
830 "maxanimseconds" => EnvVar(self::EnvVar::Maxanimseconds),
831 _ => {
833 if w.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
834 Label(w.to_string())
835 } else {
836 Name(w.to_string())
837 }
838 }
839 }
840}
841
842fn dot_keyword(w: &str) -> Option<Token> {
845 use Token::*;
846 let t = match w {
847 "PS" => DotPS,
848 "PE" => DotPE,
849 "x" => DotX,
850 "y" => DotY,
851 "ne" => Corner(self::Corner::Ne),
853 "se" => Corner(self::Corner::Se),
854 "nw" => Corner(self::Corner::Nw),
855 "sw" => Corner(self::Corner::Sw),
856 "n" | "t" | "top" | "north" => Corner(self::Corner::N),
857 "s" | "b" | "bot" | "bottom" | "south" => Corner(self::Corner::S),
858 "e" | "r" | "east" | "right" => Corner(self::Corner::E),
859 "w" | "l" | "west" | "left" => Corner(self::Corner::W),
860 "start" => Corner(self::Corner::Start),
861 "end" => Corner(self::Corner::End),
862 "c" | "center" | "centre" => Corner(self::Corner::Center),
863 "ht" | "height" => Param(self::Param::Height),
865 "wid" | "width" => Param(self::Param::Width),
866 "rad" | "radius" => Param(self::Param::Radius),
867 "diam" | "diameter" => Param(self::Param::Diameter),
868 "thick" | "thickness" => Param(self::Param::Thickness),
869 "len" | "length" => Param(self::Param::Length),
870 _ => return None,
871 };
872 Some(t)
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878
879 fn toks(src: &str) -> Vec<Token> {
880 lex(src).unwrap().into_iter().map(|s| s.tok).collect()
881 }
882
883 #[test]
884 fn basic_box() {
885 assert_eq!(
886 toks("box \"hi\""),
887 vec![Token::Prim(Prim::Box), Token::Str("hi".into()), Token::Eof]
888 );
889 }
890
891 #[test]
892 fn label_vs_name() {
893 assert_eq!(
894 toks("Start: boxwid"),
895 vec![
896 Token::Label("Start".into()),
897 Token::Colon,
898 Token::EnvVar(EnvVar::Boxwid),
899 Token::Eof
900 ]
901 );
902 assert_eq!(toks("myvar"), vec![Token::Name("myvar".into()), Token::Eof]);
904 }
905
906 #[test]
907 fn numbers() {
908 assert_eq!(toks("0.5"), vec![Token::Float(0.5), Token::Eof]);
909 assert_eq!(toks(".25"), vec![Token::Float(0.25), Token::Eof]);
910 assert_eq!(toks("1e3"), vec![Token::Float(1000.0), Token::Eof]);
911 assert_eq!(toks("2.5e-1"), vec![Token::Float(0.25), Token::Eof]);
912 }
913
914 #[test]
915 fn rejects_non_finite_numbers() {
916 let err = lex("box wid 1e999").unwrap_err();
917 assert!(err.msg.contains("not finite"), "{err}");
918 }
919
920 #[test]
921 fn operators_and_arrows() {
922 assert_eq!(
923 toks("a := b <= c -> d <- e <-> f"),
924 vec![
925 Token::Name("a".into()),
926 Token::ColonEq,
927 Token::Name("b".into()),
928 Token::Le,
929 Token::Name("c".into()),
930 Token::Arrow(Arrow::Right),
931 Token::Name("d".into()),
932 Token::Arrow(Arrow::Left),
933 Token::Name("e".into()),
934 Token::Arrow(Arrow::Double),
935 Token::Name("f".into()),
936 Token::Eof
937 ]
938 );
939 }
940
941 #[test]
942 fn minus_is_not_arrow_without_gt() {
943 assert_eq!(
944 toks("2-3"),
945 vec![
946 Token::Float(2.0),
947 Token::Minus,
948 Token::Float(3.0),
949 Token::Eof
950 ]
951 );
952 }
953
954 #[test]
955 fn compass_and_params() {
956 assert_eq!(
957 toks("last box.ne A.ht .center"),
958 vec![
959 Token::Kw(Kw::Last),
960 Token::Prim(Prim::Box),
961 Token::Corner(Corner::Ne),
962 Token::Label("A".into()),
963 Token::Param(Param::Height),
964 Token::Corner(Corner::Center),
965 Token::Eof
966 ]
967 );
968 }
969
970 #[test]
971 fn dot_then_label() {
972 assert_eq!(
974 toks("B.A"),
975 vec![
976 Token::Label("B".into()),
977 Token::Dot,
978 Token::Label("A".into()),
979 Token::Eof
980 ]
981 );
982 }
983
984 #[test]
985 fn statement_separators_and_comments() {
986 assert_eq!(
987 toks("box; circle # a comment\narc"),
988 vec![
989 Token::Prim(Prim::Box),
990 Token::Newline,
991 Token::Prim(Prim::Circle),
992 Token::Newline,
993 Token::Prim(Prim::Arc),
994 Token::Eof
995 ]
996 );
997 }
998
999 #[test]
1000 fn shell_commands_skip_raw_line_tail() {
1001 assert_eq!(
1002 toks("sh \"echo -n \\\"print \\\\\\\"\\\" > $1_prow\"\nbox"),
1003 vec![
1004 Token::Kw(Kw::Sh),
1005 Token::Newline,
1006 Token::Prim(Prim::Box),
1007 Token::Eof
1008 ]
1009 );
1010 assert_eq!(
1011 toks("command \\foo $bad \"unterminated\ncircle"),
1012 vec![
1013 Token::Kw(Kw::Command),
1014 Token::Newline,
1015 Token::Prim(Prim::Circle),
1016 Token::Eof
1017 ]
1018 );
1019 assert_eq!(
1020 toks("sh \"sed something \\\n > tmp\"\narc"),
1021 vec![
1022 Token::Kw(Kw::Sh),
1023 Token::Newline,
1024 Token::Prim(Prim::Arc),
1025 Token::Eof
1026 ]
1027 );
1028 assert_eq!(
1029 toks("sh \"rm\";}\ncommand sprintf(\"x\", y) }\n"),
1030 vec![
1031 Token::Kw(Kw::Sh),
1032 Token::Newline,
1033 Token::RightBrace,
1034 Token::Newline,
1035 Token::Kw(Kw::Command),
1036 Token::RightBrace,
1037 Token::Newline,
1038 Token::Eof
1039 ]
1040 );
1041 assert_eq!(
1042 toks("sh \"echo -n \\\"print \\\\\"\\\" > $1_prow\"\nbox"),
1043 vec![
1044 Token::Kw(Kw::Sh),
1045 Token::Newline,
1046 Token::Prim(Prim::Box),
1047 Token::Eof
1048 ]
1049 );
1050 }
1051
1052 #[test]
1053 fn line_continuation() {
1054 assert_eq!(
1055 toks("box \\\n wid 2"),
1056 vec![
1057 Token::Prim(Prim::Box),
1058 Token::Kw(Kw::Wid),
1059 Token::Float(2.0),
1060 Token::Eof
1061 ]
1062 );
1063 }
1064
1065 #[test]
1066 fn ps_pe_and_block() {
1067 assert_eq!(
1068 toks(".PS\nbox\n.PE"),
1069 vec![
1070 Token::DotPS,
1071 Token::Newline,
1072 Token::Prim(Prim::Box),
1073 Token::Newline,
1074 Token::DotPE,
1075 Token::Eof
1076 ]
1077 );
1078 assert_eq!(toks("[]"), vec![Token::Block, Token::Eof]);
1079 assert_eq!(
1080 toks("[ box ]"),
1081 vec![
1082 Token::LeftBrack,
1083 Token::Prim(Prim::Box),
1084 Token::RightBrack,
1085 Token::Eof
1086 ]
1087 );
1088 }
1089
1090 #[test]
1091 fn macro_arg() {
1092 assert_eq!(
1093 toks("box $1"),
1094 vec![Token::Prim(Prim::Box), Token::Arg(1), Token::Eof]
1095 );
1096 }
1097
1098 #[test]
1099 fn func_and_envvar() {
1100 assert_eq!(
1101 toks("sqrt(2) atan2 scale margin topmargin"),
1102 vec![
1103 Token::Func1(Func1::Sqrt),
1104 Token::Lparen,
1105 Token::Float(2.0),
1106 Token::Rparen,
1107 Token::Func2(Func2::Atan2),
1108 Token::EnvVar(EnvVar::Scale),
1109 Token::EnvVar(EnvVar::Margin),
1110 Token::EnvVar(EnvVar::Topmargin),
1111 Token::Eof
1112 ]
1113 );
1114 }
1115
1116 #[test]
1117 fn newline_after_then_is_continuation() {
1118 assert_eq!(
1120 toks("line right then\nup"),
1121 vec![
1122 Token::Prim(Prim::Line),
1123 Token::Dir(Dir::Right),
1124 Token::Kw(Kw::Then),
1125 Token::Dir(Dir::Up),
1126 Token::Eof,
1127 ]
1128 );
1129 assert_eq!(
1130 toks("box\nup"),
1131 vec![
1132 Token::Prim(Prim::Box),
1133 Token::Newline,
1134 Token::Dir(Dir::Up),
1135 Token::Eof,
1136 ]
1137 );
1138 }
1139
1140 #[test]
1141 fn newline_before_then_is_continuation() {
1142 assert_eq!(
1144 toks("line right\nthen up"),
1145 vec![
1146 Token::Prim(Prim::Line),
1147 Token::Dir(Dir::Right),
1148 Token::Kw(Kw::Then),
1149 Token::Dir(Dir::Up),
1150 Token::Eof,
1151 ]
1152 );
1153 }
1154
1155 #[test]
1156 fn dollar_and_backslash_are_literal_text() {
1157 assert_eq!(
1160 toks("$\\beta$"),
1161 vec![
1162 Token::Dollar,
1163 Token::Backslash,
1164 Token::Name("beta".into()),
1165 Token::Dollar,
1166 Token::Eof,
1167 ]
1168 );
1169 assert_eq!(toks("$1"), vec![Token::Arg(1), Token::Eof]);
1171 }
1172}