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 let mut s = String::new();
472 if self.peek() == Some('.') {
473 s.push('0'); }
475 while let Some(c) = self.peek() {
476 if c.is_ascii_digit() {
477 s.push(c);
478 self.bump();
479 } else {
480 break;
481 }
482 }
483 if self.peek() == Some('.') && self.peek_at(1).is_some_and(|d| d.is_ascii_digit()) {
486 s.push('.');
487 self.bump();
488 while let Some(c) = self.peek() {
489 if c.is_ascii_digit() {
490 s.push(c);
491 self.bump();
492 } else {
493 break;
494 }
495 }
496 }
497 if matches!(self.peek(), Some('e') | Some('E'))
499 && (self.peek_at(1).is_some_and(|d| d.is_ascii_digit())
500 || (matches!(self.peek_at(1), Some('+') | Some('-'))
501 && self.peek_at(2).is_some_and(|d| d.is_ascii_digit())))
502 {
503 s.push('e');
504 self.bump();
505 if matches!(self.peek(), Some('+') | Some('-')) {
506 s.push(self.bump().unwrap());
507 }
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('i') | Some('I'))
521 && !self
522 .peek_at(1)
523 .is_some_and(|c| c.is_alphanumeric() || c == '_')
524 {
525 self.bump();
526 }
527 match s.parse::<f64>() {
528 Ok(v) if v.is_finite() => Ok(Token::Float(v)),
529 Ok(_) => self.err(format!("number `{s}` is not finite")),
530 Err(_) => self.err(format!("invalid number `{s}`")),
531 }
532 }
533
534 fn lex_dot(&mut self) -> Result<Token, LexError> {
537 let save = (self.pos, self.line, self.col);
538 self.bump(); let mut w = String::new();
541 while let Some(c) = self.peek() {
542 if c.is_alphanumeric() || c == '_' {
543 w.push(c);
544 self.bump();
545 } else {
546 break;
547 }
548 }
549 if let Some(tok) = dot_keyword(&w) {
550 Ok(tok)
551 } else {
552 self.pos = save.0 + 1;
555 self.line = save.1;
556 self.col = save.2 + 1;
557 Ok(Token::Dot)
558 }
559 }
560
561 fn lex_word(&mut self) -> Token {
562 let mut w = String::new();
563 while let Some(c) = self.peek() {
564 if c.is_alphanumeric() || c == '_' {
565 w.push(c);
566 self.bump();
567 } else {
568 break;
569 }
570 }
571 word_keyword(&w)
572 }
573
574 fn lex_operator(&mut self) -> Result<Option<Token>, LexError> {
575 let c = self.bump().unwrap();
576 let next = self.peek();
577 let tok = match c {
578 '(' => Token::Lparen,
579 ')' => Token::Rparen,
580 ',' => Token::Comma,
581 '^' => Token::Caret,
582 '{' => Token::LeftBrace,
583 '}' => Token::RightBrace,
584 ']' => Token::RightBrack,
585 '`' => Token::LeftQuote,
586 '\'' => Token::RightQuote,
587 '[' => {
588 if next == Some(']') {
589 self.bump();
590 Token::Block
591 } else {
592 Token::LeftBrack
593 }
594 }
595 ':' => self.two('=', Token::ColonEq, Token::Colon),
596 '=' => self.two('=', Token::EqEq, Token::Eq),
597 '!' => self.two('=', Token::Neq, Token::Not),
598 '+' => self.two('=', Token::PlusEq, Token::Plus),
599 '*' => self.two('=', Token::MultEq, Token::Mult),
600 '/' => self.two('=', Token::DivEq, Token::Div),
601 '%' => self.two('=', Token::RemEq, Token::Percent),
602 '>' => self.two('=', Token::Ge, Token::Gt),
603 '&' => self.two('&', Token::AndAnd, Token::Ampersand),
604 '|' => {
605 if next == Some('|') {
606 self.bump();
607 Token::OrOr
608 } else {
609 return self.err("unexpected `|`");
610 }
611 }
612 '-' => match next {
613 Some('>') => {
614 self.bump();
615 Token::Arrow(Arrow::Right)
616 }
617 Some('=') => {
618 self.bump();
619 Token::MinusEq
620 }
621 _ => Token::Minus,
622 },
623 '<' => match next {
624 Some('=') => {
625 self.bump();
626 Token::Le
627 }
628 Some('-') => {
629 self.bump();
630 if self.peek() == Some('>') {
631 self.bump();
632 Token::Arrow(Arrow::Double)
633 } else {
634 Token::Arrow(Arrow::Left)
635 }
636 }
637 _ => Token::Lt,
638 },
639 '\\' => Token::Backslash,
644 other => return self.err(format!("unexpected character `{other}`")),
645 };
646 Ok(Some(tok))
647 }
648
649 fn two(&mut self, c: char, yes: Token, no: Token) -> Token {
651 if self.peek() == Some(c) {
652 self.bump();
653 yes
654 } else {
655 no
656 }
657 }
658}
659
660fn word_keyword(w: &str) -> Token {
663 use Token::*;
664 match w {
665 "box" => Prim(self::Prim::Box),
667 "circle" => Prim(self::Prim::Circle),
668 "ellipse" => Prim(self::Prim::Ellipse),
669 "arc" => Prim(self::Prim::Arc),
670 "line" => Prim(self::Prim::Line),
671 "arrow" => Prim(self::Prim::Arrow),
672 "move" => Prim(self::Prim::Move),
673 "spline" => Prim(self::Prim::Spline),
674 "up" => Dir(self::Dir::Up),
676 "down" => Dir(self::Dir::Down),
677 "right" => Dir(self::Dir::Right),
678 "left" => Dir(self::Dir::Left),
679 "height" | "ht" => Kw(self::Kw::Ht),
681 "width" | "wid" => Kw(self::Kw::Wid),
682 "radius" | "rad" => Kw(self::Kw::Rad),
683 "diameter" | "diam" => Kw(self::Kw::Diam),
684 "thickness" | "thick" => Kw(self::Kw::Thick),
685 "scaled" => Kw(self::Kw::Scaled),
686 "from" => Kw(self::Kw::From),
687 "to" => Kw(self::Kw::To),
688 "at" => Kw(self::Kw::At),
689 "with" => Kw(self::Kw::With),
690 "by" => Kw(self::Kw::By),
691 "then" => Kw(self::Kw::Then),
692 "cw" => Kw(self::Kw::Cw),
693 "ccw" => Kw(self::Kw::Ccw),
694 "continue" => Kw(self::Kw::Continue),
695 "chop" => Kw(self::Kw::Chop),
696 "same" => Kw(self::Kw::Same),
697 "of" => Kw(self::Kw::Of),
698 "the" => Kw(self::Kw::The),
699 "way" => Kw(self::Kw::Way),
700 "between" => Kw(self::Kw::Between),
701 "and" => Kw(self::Kw::And),
702 "last" => Kw(self::Kw::Last),
703 "fill" | "filled" => Kw(self::Kw::Fill),
704 "st" | "nd" | "rd" | "th" => Kw(self::Kw::Nth),
705 "Here" => Kw(self::Kw::Here),
706 "top" => Corner(self::Corner::N),
708 "bottom" => Corner(self::Corner::S),
709 "start" => Corner(self::Corner::Start),
710 "end" => Corner(self::Corner::End),
711 "print" => Kw(self::Kw::Print),
713 "copy" => Kw(self::Kw::Copy),
714 "reset" => Kw(self::Kw::Reset),
715 "exec" => Kw(self::Kw::Exec),
716 "sh" => Kw(self::Kw::Sh),
717 "command" => Kw(self::Kw::Command),
718 "define" => Kw(self::Kw::Define),
719 "undefine" | "undef" => Kw(self::Kw::Undef),
720 "rand" => Kw(self::Kw::Rand),
721 "if" => Kw(self::Kw::If),
722 "else" => Kw(self::Kw::Else),
723 "for" => Kw(self::Kw::For),
724 "do" => Kw(self::Kw::Do),
725 "sprintf" => Kw(self::Kw::Sprintf),
726 "animate" => Kw(self::Kw::Animate),
728 "after" => Kw(self::Kw::After),
729 "delay" => Kw(self::Kw::Delay),
730 "solid" => LineType(self::LineType::Solid),
732 "dotted" => LineType(self::LineType::Dotted),
733 "dashed" => LineType(self::LineType::Dashed),
734 "invis" | "invisible" => LineType(self::LineType::Invis),
735 "color" | "colour" | "colored" | "coloured" => Color(self::Color::Colored),
737 "outline" | "outlined" => Color(self::Color::Outlined),
738 "shade" | "shaded" => Color(self::Color::Shaded),
739 "center" | "centre" => TextPos(self::TextPos::Center),
741 "ljust" => TextPos(self::TextPos::Ljust),
742 "rjust" => TextPos(self::TextPos::Rjust),
743 "above" => TextPos(self::TextPos::Above),
744 "below" => TextPos(self::TextPos::Below),
745 "abs" => Func1(self::Func1::Abs),
747 "acos" => Func1(self::Func1::Acos),
748 "asin" => Func1(self::Func1::Asin),
749 "cos" => Func1(self::Func1::Cos),
750 "exp" => Func1(self::Func1::Exp),
751 "expe" => Func1(self::Func1::Expe),
752 "int" => Func1(self::Func1::Int),
753 "log" => Func1(self::Func1::Log),
754 "loge" => Func1(self::Func1::Loge),
755 "sign" => Func1(self::Func1::Sign),
756 "sin" => Func1(self::Func1::Sin),
757 "sqrt" => Func1(self::Func1::Sqrt),
758 "tan" => Func1(self::Func1::Tan),
759 "floor" => Func1(self::Func1::Floor),
760 "atan2" => Func2(self::Func2::Atan2),
762 "max" => Func2(self::Func2::Max),
763 "min" => Func2(self::Func2::Min),
764 "pmod" => Func2(self::Func2::Pmod),
765 "arcrad" => EnvVar(self::EnvVar::Arcrad),
767 "arrowht" => EnvVar(self::EnvVar::Arrowht),
768 "arrowwid" => EnvVar(self::EnvVar::Arrowwid),
769 "boxht" => EnvVar(self::EnvVar::Boxht),
770 "boxrad" => EnvVar(self::EnvVar::Boxrad),
771 "boxwid" => EnvVar(self::EnvVar::Boxwid),
772 "circlerad" => EnvVar(self::EnvVar::Circlerad),
773 "dashwid" => EnvVar(self::EnvVar::Dashwid),
774 "ellipseht" => EnvVar(self::EnvVar::Ellipseht),
775 "ellipsewid" => EnvVar(self::EnvVar::Ellipsewid),
776 "lineht" => EnvVar(self::EnvVar::Lineht),
777 "linewid" => EnvVar(self::EnvVar::Linewid),
778 "moveht" => EnvVar(self::EnvVar::Moveht),
779 "movewid" => EnvVar(self::EnvVar::Movewid),
780 "textht" => EnvVar(self::EnvVar::Textht),
781 "textoffset" => EnvVar(self::EnvVar::Textoffset),
782 "textwid" => EnvVar(self::EnvVar::Textwid),
783 "arrowhead" => EnvVar(self::EnvVar::Arrowhead),
784 "fillval" => EnvVar(self::EnvVar::Fillval),
785 "linethick" => EnvVar(self::EnvVar::Linethick),
786 "margin" => EnvVar(self::EnvVar::Margin),
787 "topmargin" => EnvVar(self::EnvVar::Topmargin),
788 "rightmargin" => EnvVar(self::EnvVar::Rightmargin),
789 "bottommargin" => EnvVar(self::EnvVar::Bottommargin),
790 "leftmargin" => EnvVar(self::EnvVar::Leftmargin),
791 "maxpsht" => EnvVar(self::EnvVar::Maxpsht),
792 "maxpswid" => EnvVar(self::EnvVar::Maxpswid),
793 "scale" => EnvVar(self::EnvVar::Scale),
794 "texlabels" => EnvVar(self::EnvVar::Texlabels),
795 "dotrad" => EnvVar(self::EnvVar::Dotrad),
796 _ => {
798 if w.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
799 Label(w.to_string())
800 } else {
801 Name(w.to_string())
802 }
803 }
804 }
805}
806
807fn dot_keyword(w: &str) -> Option<Token> {
810 use Token::*;
811 let t = match w {
812 "PS" => DotPS,
813 "PE" => DotPE,
814 "x" => DotX,
815 "y" => DotY,
816 "ne" => Corner(self::Corner::Ne),
818 "se" => Corner(self::Corner::Se),
819 "nw" => Corner(self::Corner::Nw),
820 "sw" => Corner(self::Corner::Sw),
821 "n" | "t" | "top" | "north" => Corner(self::Corner::N),
822 "s" | "b" | "bot" | "bottom" | "south" => Corner(self::Corner::S),
823 "e" | "r" | "east" | "right" => Corner(self::Corner::E),
824 "w" | "l" | "west" | "left" => Corner(self::Corner::W),
825 "start" => Corner(self::Corner::Start),
826 "end" => Corner(self::Corner::End),
827 "c" | "center" | "centre" => Corner(self::Corner::Center),
828 "ht" | "height" => Param(self::Param::Height),
830 "wid" | "width" => Param(self::Param::Width),
831 "rad" | "radius" => Param(self::Param::Radius),
832 "diam" | "diameter" => Param(self::Param::Diameter),
833 "thick" | "thickness" => Param(self::Param::Thickness),
834 "len" | "length" => Param(self::Param::Length),
835 _ => return None,
836 };
837 Some(t)
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843
844 fn toks(src: &str) -> Vec<Token> {
845 lex(src).unwrap().into_iter().map(|s| s.tok).collect()
846 }
847
848 #[test]
849 fn basic_box() {
850 assert_eq!(
851 toks("box \"hi\""),
852 vec![Token::Prim(Prim::Box), Token::Str("hi".into()), Token::Eof]
853 );
854 }
855
856 #[test]
857 fn label_vs_name() {
858 assert_eq!(
859 toks("Start: boxwid"),
860 vec![
861 Token::Label("Start".into()),
862 Token::Colon,
863 Token::EnvVar(EnvVar::Boxwid),
864 Token::Eof
865 ]
866 );
867 assert_eq!(toks("myvar"), vec![Token::Name("myvar".into()), Token::Eof]);
869 }
870
871 #[test]
872 fn numbers() {
873 assert_eq!(toks("0.5"), vec![Token::Float(0.5), Token::Eof]);
874 assert_eq!(toks(".25"), vec![Token::Float(0.25), Token::Eof]);
875 assert_eq!(toks("1e3"), vec![Token::Float(1000.0), Token::Eof]);
876 assert_eq!(toks("2.5e-1"), vec![Token::Float(0.25), Token::Eof]);
877 }
878
879 #[test]
880 fn rejects_non_finite_numbers() {
881 let err = lex("box wid 1e999").unwrap_err();
882 assert!(err.msg.contains("not finite"), "{err}");
883 }
884
885 #[test]
886 fn operators_and_arrows() {
887 assert_eq!(
888 toks("a := b <= c -> d <- e <-> f"),
889 vec![
890 Token::Name("a".into()),
891 Token::ColonEq,
892 Token::Name("b".into()),
893 Token::Le,
894 Token::Name("c".into()),
895 Token::Arrow(Arrow::Right),
896 Token::Name("d".into()),
897 Token::Arrow(Arrow::Left),
898 Token::Name("e".into()),
899 Token::Arrow(Arrow::Double),
900 Token::Name("f".into()),
901 Token::Eof
902 ]
903 );
904 }
905
906 #[test]
907 fn minus_is_not_arrow_without_gt() {
908 assert_eq!(
909 toks("2-3"),
910 vec![
911 Token::Float(2.0),
912 Token::Minus,
913 Token::Float(3.0),
914 Token::Eof
915 ]
916 );
917 }
918
919 #[test]
920 fn compass_and_params() {
921 assert_eq!(
922 toks("last box.ne A.ht .center"),
923 vec![
924 Token::Kw(Kw::Last),
925 Token::Prim(Prim::Box),
926 Token::Corner(Corner::Ne),
927 Token::Label("A".into()),
928 Token::Param(Param::Height),
929 Token::Corner(Corner::Center),
930 Token::Eof
931 ]
932 );
933 }
934
935 #[test]
936 fn dot_then_label() {
937 assert_eq!(
939 toks("B.A"),
940 vec![
941 Token::Label("B".into()),
942 Token::Dot,
943 Token::Label("A".into()),
944 Token::Eof
945 ]
946 );
947 }
948
949 #[test]
950 fn statement_separators_and_comments() {
951 assert_eq!(
952 toks("box; circle # a comment\narc"),
953 vec![
954 Token::Prim(Prim::Box),
955 Token::Newline,
956 Token::Prim(Prim::Circle),
957 Token::Newline,
958 Token::Prim(Prim::Arc),
959 Token::Eof
960 ]
961 );
962 }
963
964 #[test]
965 fn shell_commands_skip_raw_line_tail() {
966 assert_eq!(
967 toks("sh \"echo -n \\\"print \\\\\\\"\\\" > $1_prow\"\nbox"),
968 vec![
969 Token::Kw(Kw::Sh),
970 Token::Newline,
971 Token::Prim(Prim::Box),
972 Token::Eof
973 ]
974 );
975 assert_eq!(
976 toks("command \\foo $bad \"unterminated\ncircle"),
977 vec![
978 Token::Kw(Kw::Command),
979 Token::Newline,
980 Token::Prim(Prim::Circle),
981 Token::Eof
982 ]
983 );
984 assert_eq!(
985 toks("sh \"sed something \\\n > tmp\"\narc"),
986 vec![
987 Token::Kw(Kw::Sh),
988 Token::Newline,
989 Token::Prim(Prim::Arc),
990 Token::Eof
991 ]
992 );
993 assert_eq!(
994 toks("sh \"rm\";}\ncommand sprintf(\"x\", y) }\n"),
995 vec![
996 Token::Kw(Kw::Sh),
997 Token::Newline,
998 Token::RightBrace,
999 Token::Newline,
1000 Token::Kw(Kw::Command),
1001 Token::RightBrace,
1002 Token::Newline,
1003 Token::Eof
1004 ]
1005 );
1006 assert_eq!(
1007 toks("sh \"echo -n \\\"print \\\\\"\\\" > $1_prow\"\nbox"),
1008 vec![
1009 Token::Kw(Kw::Sh),
1010 Token::Newline,
1011 Token::Prim(Prim::Box),
1012 Token::Eof
1013 ]
1014 );
1015 }
1016
1017 #[test]
1018 fn line_continuation() {
1019 assert_eq!(
1020 toks("box \\\n wid 2"),
1021 vec![
1022 Token::Prim(Prim::Box),
1023 Token::Kw(Kw::Wid),
1024 Token::Float(2.0),
1025 Token::Eof
1026 ]
1027 );
1028 }
1029
1030 #[test]
1031 fn ps_pe_and_block() {
1032 assert_eq!(
1033 toks(".PS\nbox\n.PE"),
1034 vec![
1035 Token::DotPS,
1036 Token::Newline,
1037 Token::Prim(Prim::Box),
1038 Token::Newline,
1039 Token::DotPE,
1040 Token::Eof
1041 ]
1042 );
1043 assert_eq!(toks("[]"), vec![Token::Block, Token::Eof]);
1044 assert_eq!(
1045 toks("[ box ]"),
1046 vec![
1047 Token::LeftBrack,
1048 Token::Prim(Prim::Box),
1049 Token::RightBrack,
1050 Token::Eof
1051 ]
1052 );
1053 }
1054
1055 #[test]
1056 fn macro_arg() {
1057 assert_eq!(
1058 toks("box $1"),
1059 vec![Token::Prim(Prim::Box), Token::Arg(1), Token::Eof]
1060 );
1061 }
1062
1063 #[test]
1064 fn func_and_envvar() {
1065 assert_eq!(
1066 toks("sqrt(2) atan2 scale margin topmargin"),
1067 vec![
1068 Token::Func1(Func1::Sqrt),
1069 Token::Lparen,
1070 Token::Float(2.0),
1071 Token::Rparen,
1072 Token::Func2(Func2::Atan2),
1073 Token::EnvVar(EnvVar::Scale),
1074 Token::EnvVar(EnvVar::Margin),
1075 Token::EnvVar(EnvVar::Topmargin),
1076 Token::Eof
1077 ]
1078 );
1079 }
1080
1081 #[test]
1082 fn newline_after_then_is_continuation() {
1083 assert_eq!(
1085 toks("line right then\nup"),
1086 vec![
1087 Token::Prim(Prim::Line),
1088 Token::Dir(Dir::Right),
1089 Token::Kw(Kw::Then),
1090 Token::Dir(Dir::Up),
1091 Token::Eof,
1092 ]
1093 );
1094 assert_eq!(
1095 toks("box\nup"),
1096 vec![
1097 Token::Prim(Prim::Box),
1098 Token::Newline,
1099 Token::Dir(Dir::Up),
1100 Token::Eof,
1101 ]
1102 );
1103 }
1104
1105 #[test]
1106 fn newline_before_then_is_continuation() {
1107 assert_eq!(
1109 toks("line right\nthen up"),
1110 vec![
1111 Token::Prim(Prim::Line),
1112 Token::Dir(Dir::Right),
1113 Token::Kw(Kw::Then),
1114 Token::Dir(Dir::Up),
1115 Token::Eof,
1116 ]
1117 );
1118 }
1119
1120 #[test]
1121 fn dollar_and_backslash_are_literal_text() {
1122 assert_eq!(
1125 toks("$\\beta$"),
1126 vec![
1127 Token::Dollar,
1128 Token::Backslash,
1129 Token::Name("beta".into()),
1130 Token::Dollar,
1131 Token::Eof,
1132 ]
1133 );
1134 assert_eq!(toks("$1"), vec![Token::Arg(1), Token::Eof]);
1136 }
1137}