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<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: &[Vec<Spanned>]) -> Self {
61 self.arg_frame = Some(args.to_vec());
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 "scaled" => Kw(self::Kw::Scaled),
706 "from" => Kw(self::Kw::From),
707 "to" => Kw(self::Kw::To),
708 "at" => Kw(self::Kw::At),
709 "with" => Kw(self::Kw::With),
710 "by" => Kw(self::Kw::By),
711 "then" => Kw(self::Kw::Then),
712 "cw" => Kw(self::Kw::Cw),
713 "ccw" => Kw(self::Kw::Ccw),
714 "continue" => Kw(self::Kw::Continue),
715 "chop" => Kw(self::Kw::Chop),
716 "same" => Kw(self::Kw::Same),
717 "of" => Kw(self::Kw::Of),
718 "the" => Kw(self::Kw::The),
719 "way" => Kw(self::Kw::Way),
720 "between" => Kw(self::Kw::Between),
721 "and" => Kw(self::Kw::And),
722 "last" => Kw(self::Kw::Last),
723 "fill" | "filled" => Kw(self::Kw::Fill),
724 "st" | "nd" | "rd" | "th" => Kw(self::Kw::Nth),
725 "Here" => Kw(self::Kw::Here),
726 "top" => Corner(self::Corner::N),
728 "bottom" => Corner(self::Corner::S),
729 "start" => Corner(self::Corner::Start),
730 "end" => Corner(self::Corner::End),
731 "print" => Kw(self::Kw::Print),
733 "copy" => Kw(self::Kw::Copy),
734 "reset" => Kw(self::Kw::Reset),
735 "exec" => Kw(self::Kw::Exec),
736 "sh" => Kw(self::Kw::Sh),
737 "command" => Kw(self::Kw::Command),
738 "define" => Kw(self::Kw::Define),
739 "undefine" | "undef" => Kw(self::Kw::Undef),
740 "rand" => Kw(self::Kw::Rand),
741 "if" => Kw(self::Kw::If),
742 "else" => Kw(self::Kw::Else),
743 "for" => Kw(self::Kw::For),
744 "do" => Kw(self::Kw::Do),
745 "sprintf" => Kw(self::Kw::Sprintf),
746 "animate" => Kw(self::Kw::Animate),
748 "after" => Kw(self::Kw::After),
749 "delay" => Kw(self::Kw::Delay),
750 "solid" => LineType(self::LineType::Solid),
752 "dotted" => LineType(self::LineType::Dotted),
753 "dashed" => LineType(self::LineType::Dashed),
754 "invis" | "invisible" => LineType(self::LineType::Invis),
755 "color" | "colour" | "colored" | "coloured" => Color(self::Color::Colored),
757 "outline" | "outlined" => Color(self::Color::Outlined),
758 "shade" | "shaded" => Color(self::Color::Shaded),
759 "center" | "centre" => TextPos(self::TextPos::Center),
761 "ljust" => TextPos(self::TextPos::Ljust),
762 "rjust" => TextPos(self::TextPos::Rjust),
763 "above" => TextPos(self::TextPos::Above),
764 "below" => TextPos(self::TextPos::Below),
765 "abs" => Func1(self::Func1::Abs),
767 "acos" => Func1(self::Func1::Acos),
768 "asin" => Func1(self::Func1::Asin),
769 "cos" => Func1(self::Func1::Cos),
770 "exp" => Func1(self::Func1::Exp),
771 "expe" => Func1(self::Func1::Expe),
772 "int" => Func1(self::Func1::Int),
773 "log" => Func1(self::Func1::Log),
774 "loge" => Func1(self::Func1::Loge),
775 "sign" => Func1(self::Func1::Sign),
776 "sin" => Func1(self::Func1::Sin),
777 "sqrt" => Func1(self::Func1::Sqrt),
778 "tan" => Func1(self::Func1::Tan),
779 "floor" => Func1(self::Func1::Floor),
780 "atan2" => Func2(self::Func2::Atan2),
782 "max" => Func2(self::Func2::Max),
783 "min" => Func2(self::Func2::Min),
784 "pmod" => Func2(self::Func2::Pmod),
785 "arcrad" => EnvVar(self::EnvVar::Arcrad),
787 "arrowht" => EnvVar(self::EnvVar::Arrowht),
788 "arrowwid" => EnvVar(self::EnvVar::Arrowwid),
789 "boxht" => EnvVar(self::EnvVar::Boxht),
790 "boxrad" => EnvVar(self::EnvVar::Boxrad),
791 "boxwid" => EnvVar(self::EnvVar::Boxwid),
792 "circlerad" => EnvVar(self::EnvVar::Circlerad),
793 "dashwid" => EnvVar(self::EnvVar::Dashwid),
794 "ellipseht" => EnvVar(self::EnvVar::Ellipseht),
795 "ellipsewid" => EnvVar(self::EnvVar::Ellipsewid),
796 "lineht" => EnvVar(self::EnvVar::Lineht),
797 "linewid" => EnvVar(self::EnvVar::Linewid),
798 "moveht" => EnvVar(self::EnvVar::Moveht),
799 "movewid" => EnvVar(self::EnvVar::Movewid),
800 "textht" => EnvVar(self::EnvVar::Textht),
801 "textoffset" => EnvVar(self::EnvVar::Textoffset),
802 "textwid" => EnvVar(self::EnvVar::Textwid),
803 "arrowhead" => EnvVar(self::EnvVar::Arrowhead),
804 "fillval" => EnvVar(self::EnvVar::Fillval),
805 "linethick" => EnvVar(self::EnvVar::Linethick),
806 "margin" => EnvVar(self::EnvVar::Margin),
807 "topmargin" => EnvVar(self::EnvVar::Topmargin),
808 "rightmargin" => EnvVar(self::EnvVar::Rightmargin),
809 "bottommargin" => EnvVar(self::EnvVar::Bottommargin),
810 "leftmargin" => EnvVar(self::EnvVar::Leftmargin),
811 "maxpsht" => EnvVar(self::EnvVar::Maxpsht),
812 "maxpswid" => EnvVar(self::EnvVar::Maxpswid),
813 "scale" => EnvVar(self::EnvVar::Scale),
814 "texlabels" => EnvVar(self::EnvVar::Texlabels),
815 "dotrad" => EnvVar(self::EnvVar::Dotrad),
816 _ => {
818 if w.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
819 Label(w.to_string())
820 } else {
821 Name(w.to_string())
822 }
823 }
824 }
825}
826
827fn dot_keyword(w: &str) -> Option<Token> {
830 use Token::*;
831 let t = match w {
832 "PS" => DotPS,
833 "PE" => DotPE,
834 "x" => DotX,
835 "y" => DotY,
836 "ne" => Corner(self::Corner::Ne),
838 "se" => Corner(self::Corner::Se),
839 "nw" => Corner(self::Corner::Nw),
840 "sw" => Corner(self::Corner::Sw),
841 "n" | "t" | "top" | "north" => Corner(self::Corner::N),
842 "s" | "b" | "bot" | "bottom" | "south" => Corner(self::Corner::S),
843 "e" | "r" | "east" | "right" => Corner(self::Corner::E),
844 "w" | "l" | "west" | "left" => Corner(self::Corner::W),
845 "start" => Corner(self::Corner::Start),
846 "end" => Corner(self::Corner::End),
847 "c" | "center" | "centre" => Corner(self::Corner::Center),
848 "ht" | "height" => Param(self::Param::Height),
850 "wid" | "width" => Param(self::Param::Width),
851 "rad" | "radius" => Param(self::Param::Radius),
852 "diam" | "diameter" => Param(self::Param::Diameter),
853 "thick" | "thickness" => Param(self::Param::Thickness),
854 "len" | "length" => Param(self::Param::Length),
855 _ => return None,
856 };
857 Some(t)
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 fn toks(src: &str) -> Vec<Token> {
865 lex(src).unwrap().into_iter().map(|s| s.tok).collect()
866 }
867
868 #[test]
869 fn basic_box() {
870 assert_eq!(
871 toks("box \"hi\""),
872 vec![Token::Prim(Prim::Box), Token::Str("hi".into()), Token::Eof]
873 );
874 }
875
876 #[test]
877 fn label_vs_name() {
878 assert_eq!(
879 toks("Start: boxwid"),
880 vec![
881 Token::Label("Start".into()),
882 Token::Colon,
883 Token::EnvVar(EnvVar::Boxwid),
884 Token::Eof
885 ]
886 );
887 assert_eq!(toks("myvar"), vec![Token::Name("myvar".into()), Token::Eof]);
889 }
890
891 #[test]
892 fn numbers() {
893 assert_eq!(toks("0.5"), vec![Token::Float(0.5), Token::Eof]);
894 assert_eq!(toks(".25"), vec![Token::Float(0.25), Token::Eof]);
895 assert_eq!(toks("1e3"), vec![Token::Float(1000.0), Token::Eof]);
896 assert_eq!(toks("2.5e-1"), vec![Token::Float(0.25), Token::Eof]);
897 }
898
899 #[test]
900 fn rejects_non_finite_numbers() {
901 let err = lex("box wid 1e999").unwrap_err();
902 assert!(err.msg.contains("not finite"), "{err}");
903 }
904
905 #[test]
906 fn operators_and_arrows() {
907 assert_eq!(
908 toks("a := b <= c -> d <- e <-> f"),
909 vec![
910 Token::Name("a".into()),
911 Token::ColonEq,
912 Token::Name("b".into()),
913 Token::Le,
914 Token::Name("c".into()),
915 Token::Arrow(Arrow::Right),
916 Token::Name("d".into()),
917 Token::Arrow(Arrow::Left),
918 Token::Name("e".into()),
919 Token::Arrow(Arrow::Double),
920 Token::Name("f".into()),
921 Token::Eof
922 ]
923 );
924 }
925
926 #[test]
927 fn minus_is_not_arrow_without_gt() {
928 assert_eq!(
929 toks("2-3"),
930 vec![
931 Token::Float(2.0),
932 Token::Minus,
933 Token::Float(3.0),
934 Token::Eof
935 ]
936 );
937 }
938
939 #[test]
940 fn compass_and_params() {
941 assert_eq!(
942 toks("last box.ne A.ht .center"),
943 vec![
944 Token::Kw(Kw::Last),
945 Token::Prim(Prim::Box),
946 Token::Corner(Corner::Ne),
947 Token::Label("A".into()),
948 Token::Param(Param::Height),
949 Token::Corner(Corner::Center),
950 Token::Eof
951 ]
952 );
953 }
954
955 #[test]
956 fn dot_then_label() {
957 assert_eq!(
959 toks("B.A"),
960 vec![
961 Token::Label("B".into()),
962 Token::Dot,
963 Token::Label("A".into()),
964 Token::Eof
965 ]
966 );
967 }
968
969 #[test]
970 fn statement_separators_and_comments() {
971 assert_eq!(
972 toks("box; circle # a comment\narc"),
973 vec![
974 Token::Prim(Prim::Box),
975 Token::Newline,
976 Token::Prim(Prim::Circle),
977 Token::Newline,
978 Token::Prim(Prim::Arc),
979 Token::Eof
980 ]
981 );
982 }
983
984 #[test]
985 fn shell_commands_skip_raw_line_tail() {
986 assert_eq!(
987 toks("sh \"echo -n \\\"print \\\\\\\"\\\" > $1_prow\"\nbox"),
988 vec![
989 Token::Kw(Kw::Sh),
990 Token::Newline,
991 Token::Prim(Prim::Box),
992 Token::Eof
993 ]
994 );
995 assert_eq!(
996 toks("command \\foo $bad \"unterminated\ncircle"),
997 vec![
998 Token::Kw(Kw::Command),
999 Token::Newline,
1000 Token::Prim(Prim::Circle),
1001 Token::Eof
1002 ]
1003 );
1004 assert_eq!(
1005 toks("sh \"sed something \\\n > tmp\"\narc"),
1006 vec![
1007 Token::Kw(Kw::Sh),
1008 Token::Newline,
1009 Token::Prim(Prim::Arc),
1010 Token::Eof
1011 ]
1012 );
1013 assert_eq!(
1014 toks("sh \"rm\";}\ncommand sprintf(\"x\", y) }\n"),
1015 vec![
1016 Token::Kw(Kw::Sh),
1017 Token::Newline,
1018 Token::RightBrace,
1019 Token::Newline,
1020 Token::Kw(Kw::Command),
1021 Token::RightBrace,
1022 Token::Newline,
1023 Token::Eof
1024 ]
1025 );
1026 assert_eq!(
1027 toks("sh \"echo -n \\\"print \\\\\"\\\" > $1_prow\"\nbox"),
1028 vec![
1029 Token::Kw(Kw::Sh),
1030 Token::Newline,
1031 Token::Prim(Prim::Box),
1032 Token::Eof
1033 ]
1034 );
1035 }
1036
1037 #[test]
1038 fn line_continuation() {
1039 assert_eq!(
1040 toks("box \\\n wid 2"),
1041 vec![
1042 Token::Prim(Prim::Box),
1043 Token::Kw(Kw::Wid),
1044 Token::Float(2.0),
1045 Token::Eof
1046 ]
1047 );
1048 }
1049
1050 #[test]
1051 fn ps_pe_and_block() {
1052 assert_eq!(
1053 toks(".PS\nbox\n.PE"),
1054 vec![
1055 Token::DotPS,
1056 Token::Newline,
1057 Token::Prim(Prim::Box),
1058 Token::Newline,
1059 Token::DotPE,
1060 Token::Eof
1061 ]
1062 );
1063 assert_eq!(toks("[]"), vec![Token::Block, Token::Eof]);
1064 assert_eq!(
1065 toks("[ box ]"),
1066 vec![
1067 Token::LeftBrack,
1068 Token::Prim(Prim::Box),
1069 Token::RightBrack,
1070 Token::Eof
1071 ]
1072 );
1073 }
1074
1075 #[test]
1076 fn macro_arg() {
1077 assert_eq!(
1078 toks("box $1"),
1079 vec![Token::Prim(Prim::Box), Token::Arg(1), Token::Eof]
1080 );
1081 }
1082
1083 #[test]
1084 fn func_and_envvar() {
1085 assert_eq!(
1086 toks("sqrt(2) atan2 scale margin topmargin"),
1087 vec![
1088 Token::Func1(Func1::Sqrt),
1089 Token::Lparen,
1090 Token::Float(2.0),
1091 Token::Rparen,
1092 Token::Func2(Func2::Atan2),
1093 Token::EnvVar(EnvVar::Scale),
1094 Token::EnvVar(EnvVar::Margin),
1095 Token::EnvVar(EnvVar::Topmargin),
1096 Token::Eof
1097 ]
1098 );
1099 }
1100
1101 #[test]
1102 fn newline_after_then_is_continuation() {
1103 assert_eq!(
1105 toks("line right then\nup"),
1106 vec![
1107 Token::Prim(Prim::Line),
1108 Token::Dir(Dir::Right),
1109 Token::Kw(Kw::Then),
1110 Token::Dir(Dir::Up),
1111 Token::Eof,
1112 ]
1113 );
1114 assert_eq!(
1115 toks("box\nup"),
1116 vec![
1117 Token::Prim(Prim::Box),
1118 Token::Newline,
1119 Token::Dir(Dir::Up),
1120 Token::Eof,
1121 ]
1122 );
1123 }
1124
1125 #[test]
1126 fn newline_before_then_is_continuation() {
1127 assert_eq!(
1129 toks("line right\nthen up"),
1130 vec![
1131 Token::Prim(Prim::Line),
1132 Token::Dir(Dir::Right),
1133 Token::Kw(Kw::Then),
1134 Token::Dir(Dir::Up),
1135 Token::Eof,
1136 ]
1137 );
1138 }
1139
1140 #[test]
1141 fn dollar_and_backslash_are_literal_text() {
1142 assert_eq!(
1145 toks("$\\beta$"),
1146 vec![
1147 Token::Dollar,
1148 Token::Backslash,
1149 Token::Name("beta".into()),
1150 Token::Dollar,
1151 Token::Eof,
1152 ]
1153 );
1154 assert_eq!(toks("$1"), vec![Token::Arg(1), Token::Eof]);
1156 }
1157}