1use crate::span::{Loc, Span};
9use crate::token::{Atom, Token};
10use crate::version::RustyfiVersion;
11
12#[derive(Debug, thiserror::Error)]
13#[error("{span}: {msg}")]
14pub struct LexError {
15 pub span: Span,
16 pub msg: String,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20enum Mode {
21 Program,
22 Vertical,
23 Horizontal,
24 Active,
25 Math,
26}
27
28pub fn lex(src: &str) -> Result<Vec<Atom>, LexError> {
32 lex_with_version(src, RustyfiVersion::V0_0)
33}
34
35pub fn lex_with_version(src: &str, version: RustyfiVersion) -> Result<Vec<Atom>, LexError> {
42 match Lexer::new(src, Mode::Program, version).run() {
43 (atoms, None) => Ok(atoms),
44 (_, Some(e)) => Err(e),
45 }
46}
47
48pub fn lex_partial(src: &str, version: RustyfiVersion) -> (Vec<Atom>, Option<LexError>) {
68 Lexer::new(src, Mode::Program, version).run()
69}
70
71struct Lexer {
72 chars: Vec<char>,
73 pos: usize,
74 line: u32,
75 col: u32,
76 byte: usize,
77 stack: Vec<Mode>,
78 out: Vec<Atom>,
79 version: RustyfiVersion,
80}
81
82fn is_small(c: char) -> bool {
83 c.is_ascii_lowercase()
84}
85fn is_capital(c: char) -> bool {
86 c.is_ascii_uppercase()
87}
88fn is_digit(c: char) -> bool {
89 c.is_ascii_digit()
90}
91fn is_hex(c: char) -> bool {
92 c.is_ascii_digit() || ('A'..='F').contains(&c)
93}
94fn is_ident_char(c: char) -> bool {
95 c.is_ascii_alphanumeric() || c == '-'
96}
97fn is_space(c: char) -> bool {
98 c == ' ' || c == '\t'
99}
100fn is_break(c: char) -> bool {
101 c == '\n' || c == '\r'
102}
103fn is_opsymbol(c: char) -> bool {
104 matches!(
105 c,
106 '+' | '-' | '*' | '/' | '^' | '&' | '|' | '!' | ':' | '=' | '<' | '>' | '~' | '\'' | '.'
107 | '?'
108 )
109}
110fn is_symbol_char(c: char) -> bool {
112 matches!(c, ' '..='@' | '['..='`' | '{'..='~')
113}
114fn is_str_char(c: char) -> bool {
116 !matches!(
117 c,
118 ' ' | '\t' | '\n' | '\r' | '@' | '`' | '\\' | '{' | '}' | '<' | '>' | '%' | '|' | '*'
119 | '$' | '#' | ';'
120 )
121}
122fn is_mathsymbol_top(c: char) -> bool {
123 matches!(c, '+' | '-' | '*' | '/' | ':' | '=' | '<' | '>' | '~' | '.' | ',' | '`')
124}
125#[allow(dead_code)]
130fn is_mathsymbol(c: char) -> bool {
131 is_mathsymbol_top(c) || c == '?'
132}
133
134impl Lexer {
135 fn new(src: &str, initial: Mode, version: RustyfiVersion) -> Self {
136 Lexer {
137 chars: src.chars().collect(),
138 pos: 0,
139 line: 1,
140 col: 0,
141 byte: 0,
142 stack: vec![initial],
143 out: Vec::new(),
144 version,
145 }
146 }
147
148 fn keyword(&self, s: &str) -> Option<Token> {
161 use Token::*;
162 if let Some(tok) = match s {
163 "mod" => Some(Mod),
166 "if" => Some(If),
167 "then" => Some(Then),
168 "else" => Some(Else),
169 "let" => Some(Let),
170 "let-rec" => Some(LetRec),
171 "and" => Some(LetAnd),
172 "in" => Some(In),
173 "fun" => Some(Fun),
174 "true" => Some(True),
175 "false" => Some(False),
176 "before" => Some(Before),
177 "while" => Some(While),
178 "do" => Some(Do),
179 "let-mutable" => Some(LetMutable),
180 "match" => Some(Match),
181 "with" => Some(With),
182 "when" => Some(When),
183 "as" => Some(As),
184 "type" => Some(Type),
185 "of" => Some(Of),
186 "module" => Some(Module),
187 "struct" => Some(Struct),
188 "sig" => Some(Sig),
189 "val" => Some(Val),
190 "end" => Some(End),
191 "direct" => Some(Direct),
192 "constraint" => Some(Constraint),
193 "let-inline" => Some(LetHorz),
194 "let-block" => Some(LetVert),
195 "let-math" => Some(LetMath),
196 "controls" => Some(Controls),
197 "cycle" => Some(Cycle),
198 "inline-cmd" => Some(HorzCmdType),
199 "block-cmd" => Some(VertCmdType),
200 "math-cmd" => Some(MathCmdType),
201 "command" => Some(Command),
202 "open" => Some(Open),
203 _ => None,
204 } {
205 return Some(tok);
206 }
207 if self.version == RustyfiVersion::V0_1 {
208 return match s {
209 "rec" => Some(Rec),
210 "inline" => Some(Inline),
211 "block" => Some(Block),
212 "mutable" => Some(Mutable),
213 "signature" => Some(Signature),
214 "include" => Some(Include),
215 "use" => Some(Use),
216 "package" => Some(Package),
217 "math" => Some(Math),
218 "persistent" => Some(Persistent),
219 _ => None,
220 };
221 }
222 None
223 }
224
225 fn loc(&self) -> Loc {
226 Loc {
227 line: self.line,
228 col: self.col,
229 byte: self.byte,
230 }
231 }
232
233 fn peek(&self) -> Option<char> {
234 self.chars.get(self.pos).copied()
235 }
236
237 fn peek_at(&self, k: usize) -> Option<char> {
238 self.chars.get(self.pos + k).copied()
239 }
240
241 fn bump(&mut self) -> char {
242 let c = self.chars[self.pos];
243 self.pos += 1;
244 self.byte += c.len_utf8();
245 if c == '\n' || (c == '\r' && self.peek() != Some('\n')) {
246 self.line += 1;
247 self.col = 0;
248 } else {
249 self.col += 1;
250 }
251 c
252 }
253
254 fn bump_n(&mut self, n: usize) {
255 for _ in 0..n {
256 self.bump();
257 }
258 }
259
260 fn scan_while(&mut self, pred: impl Fn(char) -> bool) -> String {
261 let mut s = String::new();
262 while let Some(c) = self.peek() {
263 if pred(c) {
264 s.push(self.bump());
265 } else {
266 break;
267 }
268 }
269 s
270 }
271
272 fn count_at(&self, k: usize, pred: impl Fn(char) -> bool) -> usize {
274 let mut n = 0;
275 while self.peek_at(k + n).is_some_and(&pred) {
276 n += 1;
277 }
278 n
279 }
280
281 fn emit(&mut self, start: Loc, tok: Token) {
282 self.out.push(Atom {
283 slot: tok,
284 span: Span::new(start, self.loc()),
285 });
286 }
287
288 fn error<T>(&self, start: Loc, msg: impl Into<String>) -> Result<T, LexError> {
289 Err(LexError {
290 span: Span::new(start, self.loc()),
291 msg: msg.into(),
292 })
293 }
294
295 fn push_mode(&mut self, m: Mode) {
296 self.stack.push(m);
297 }
298
299 fn pop_mode(&mut self, start: Loc, errmsg: &str) -> Result<(), LexError> {
300 if self.stack.len() > 1 {
301 self.stack.pop();
302 Ok(())
303 } else {
304 self.error(start, errmsg)
305 }
306 }
307
308 fn comment(&mut self) {
310 while let Some(c) = self.peek() {
311 self.bump();
312 if is_break(c) {
313 break;
314 }
315 }
316 }
317
318 fn skip_spaces(&mut self) {
320 while let Some(c) = self.peek() {
321 if is_space(c) || is_break(c) {
322 self.bump();
323 } else if c == '%' {
324 self.bump();
325 self.comment();
326 } else {
327 break;
328 }
329 }
330 }
331
332 fn run(mut self) -> (Vec<Atom>, Option<LexError>) {
338 loop {
339 let step = match self.stack.last().copied().expect("mode stack never empty") {
340 Mode::Program => self.lex_program(),
341 Mode::Vertical => self.lex_vertical(),
342 Mode::Horizontal => self.lex_horizontal(),
343 Mode::Active => self.lex_active(),
344 Mode::Math => self.lex_math(),
345 };
346 if let Err(e) = step {
347 return (self.out, Some(e));
348 }
349 if matches!(self.out.last(), Some(a) if a.slot == Token::Eoi) {
350 return (self.out, None);
351 }
352 }
353 }
354
355 fn name_len_at(&self, k: usize) -> Option<usize> {
359 let c = self.peek_at(k)?;
360 if is_small(c) || is_capital(c) {
361 Some(1 + self.count_at(k + 1, is_ident_char))
362 } else {
363 None
364 }
365 }
366
367 fn ident_len_at(&self, k: usize) -> Option<usize> {
369 let c = self.peek_at(k)?;
370 if is_small(c) {
371 Some(1 + self.count_at(k + 1, is_ident_char))
372 } else {
373 None
374 }
375 }
376
377 fn take(&mut self, n: usize) -> String {
378 let mut s = String::with_capacity(n);
379 for _ in 0..n {
380 s.push(self.bump());
381 }
382 s
383 }
384
385 fn scan_dotted(&mut self) -> Option<(Vec<String>, String, bool)> {
388 let mut mods = Vec::new();
389 loop {
390 let len = self.name_len_at(0)?;
391 let is_ctor = is_capital(self.peek().unwrap());
392 if is_ctor
394 && self.peek_at(len) == Some('.')
395 && self.name_len_at(len + 1).is_some()
396 {
397 let seg = self.take(len);
398 self.bump(); mods.push(seg);
400 continue;
401 }
402 let last = self.take(len);
403 return Some((mods, last, is_ctor));
404 }
405 }
406
407 fn read_literal_body(&mut self, start: Loc, quote_len: usize) -> Result<(String, bool), LexError> {
411 let mut body = String::new();
412 loop {
413 match self.peek() {
414 None => return self.error(start, "unexpected end of input while reading literal area"),
415 Some('`') => {
416 let run = self.scan_while(|c| c == '`');
417 if run.len() < quote_len {
418 body.push_str(&run);
419 } else if run.len() > quote_len {
420 return self.error(start, "literal area was closed with too many '`'s");
421 } else {
422 let omit_post = if self.peek() == Some('#') {
423 self.bump();
424 false
425 } else {
426 true
427 };
428 return Ok((body, omit_post));
429 }
430 }
431 Some(c) => {
432 body.push(c);
433 self.bump();
434 }
435 }
436 }
437 }
438
439 fn literal(&mut self, start: Loc, quote_len: usize, omit_pre: bool) -> Result<(), LexError> {
441 let (body, omit_post) = self.read_literal_body(start, quote_len)?;
442 self.emit(start, Token::Literal { body, omit_pre, omit_post });
443 Ok(())
444 }
445
446 fn literal_horz(&mut self, start: Loc, quote_len: usize, omit_pre: bool) -> Result<(), LexError> {
458 let (body, omit_post) = self.read_literal_body(start, quote_len)?;
459 let mut text: &str = &body;
460 if omit_pre {
461 text = text.trim_start_matches(' ');
462 }
463 if omit_post {
464 text = text.trim_end_matches(' ');
465 }
466 self.emit(start, Token::CodeText(text.to_string()));
467 Ok(())
468 }
469
470 fn length_lookahead(&self) -> Option<(usize, usize, usize)> {
473 let mut k = 0;
474 if self.peek_at(k) == Some('-') {
475 k += 1;
476 }
477 let int_digits = self.count_at(k, is_digit);
478 k += int_digits;
479 let mut frac_digits = 0;
480 let mut has_dot = false;
481 if self.peek_at(k) == Some('.') {
482 has_dot = true;
483 frac_digits = self.count_at(k + 1, is_digit);
484 k += 1 + frac_digits;
485 }
486 if int_digits == 0 && frac_digits == 0 {
487 return None;
488 }
489 if int_digits == 0 && !has_dot {
491 return None;
492 }
493 let numeric_len = k;
494 let unit_len = self.ident_len_at(k)?;
495 Some((numeric_len + unit_len, numeric_len, unit_len))
496 }
497
498 fn lex_program(&mut self) -> Result<(), LexError> {
501 loop {
502 let start = self.loc();
503 let Some(c) = self.peek() else {
504 if self.stack.len() == 1 {
505 self.emit(start, Token::Eoi);
506 return Ok(());
507 }
508 return self.error(start, "text input ended while reading a program area");
509 };
510 match c {
511 '%' => {
512 self.bump();
513 self.comment();
514 }
515 _ if is_space(c) || is_break(c) => {
516 self.bump();
517 }
518 '@' => {
519 self.bump();
520 return self.lex_header(start);
521 }
522 '(' => {
523 self.bump();
524 if self.peek() == Some('|') {
525 self.bump();
526 self.push_mode(Mode::Program);
527 self.emit(start, Token::BRecord);
528 } else {
529 self.push_mode(Mode::Program);
530 self.emit(start, Token::LParen);
531 }
532 return Ok(());
533 }
534 ')' => {
535 self.bump();
536 self.pop_mode(start, "too many closing")?;
537 self.emit(start, Token::RParen);
538 return Ok(());
539 }
540 '[' => {
541 self.bump();
542 self.push_mode(Mode::Program);
543 self.emit(start, Token::BList);
544 return Ok(());
545 }
546 ']' => {
547 self.bump();
548 if self.peek() == Some('>') {
549 self.bump();
550 self.emit(start, Token::EPath);
551 } else {
552 self.pop_mode(start, "too many closing")?;
553 self.emit(start, Token::EList);
554 }
555 return Ok(());
556 }
557 ';' => {
558 self.bump();
559 self.emit(start, Token::ListPunct);
560 return Ok(());
561 }
562 '{' => {
563 self.bump();
564 self.push_mode(Mode::Horizontal);
565 self.skip_spaces();
566 self.emit(start, Token::BHorzGrp);
567 return Ok(());
568 }
569 '\'' => {
570 self.bump();
571 if self.peek() == Some('<') {
572 self.bump();
573 self.push_mode(Mode::Vertical);
574 self.emit(start, Token::BVertGrp);
575 } else if let Some(len) = self.ident_len_at(0) {
576 let name = self.take(len);
577 self.emit(start, Token::TypeVar(name));
578 } else {
579 return self.error(start, "illegal token '''");
580 }
581 return Ok(());
582 }
583 '$' => {
584 self.bump();
585 if self.peek() == Some('{') {
586 self.bump();
587 self.push_mode(Mode::Math);
588 self.emit(start, Token::BMathGrp);
589 return Ok(());
590 }
591 return self.error(start, "illegal token '$' in a program area");
592 }
593 '`' => {
594 let quotes = self.scan_while(|c| c == '`');
595 self.literal(start, quotes.len(), true)?;
596 return Ok(());
597 }
598 '#' => {
599 self.bump();
600 if self.peek() == Some('`') {
601 let quotes = self.scan_while(|c| c == '`');
602 self.literal(start, quotes.len(), false)?;
603 } else {
604 self.emit(start, Token::Access);
605 }
606 return Ok(());
607 }
608 '\\' => {
609 self.bump();
620 let Some((mods, name, _)) = self.scan_dotted() else {
621 return self.error(start, "illegal token '\\' in a program area");
622 };
623 let cmd_name = format!("\\{name}");
624 if mods.is_empty() {
625 if self.peek() == Some('@') {
626 self.bump();
627 self.emit(start, Token::HorzMacro(format!("{cmd_name}@")));
628 } else {
629 self.emit(start, Token::HorzCmd(cmd_name));
630 }
631 } else {
632 self.emit(start, Token::HorzCmdWithMod(mods, cmd_name));
633 }
634 return Ok(());
635 }
636 '+' => {
637 self.bump();
638 if let Some(len) = self.name_len_at(0) {
639 let name = format!("+{}", self.take(len));
640 if self.peek() == Some('@') {
641 self.bump();
642 self.emit(start, Token::VertMacro(format!("{name}@")));
643 } else {
644 self.emit(start, Token::VertCmd(name));
645 }
646 } else {
647 let run = format!("+{}", self.scan_while(is_opsymbol));
648 self.emit(start, Token::BinopPlus(run));
649 }
650 return Ok(());
651 }
652 ',' => {
653 self.bump();
654 self.emit(start, Token::Comma);
655 return Ok(());
656 }
657 '_' => {
658 self.bump();
659 self.emit(start, Token::Wildcard);
660 return Ok(());
661 }
662 '-' => {
663 if let Some((total, num_len, unit_len)) = self.length_lookahead() {
664 let num: String = self.take(num_len);
665 let unit: String = self.take(unit_len);
666 debug_assert_eq!(total, num.chars().count() + unit.chars().count());
667 let value: f64 = num.parse().map_err(|_| LexError {
668 span: Span::new(start, self.loc()),
669 msg: format!("malformed length constant '{num}{unit}'"),
670 })?;
671 self.emit(start, Token::LengthConst(value, unit));
672 return Ok(());
673 }
674 let run = self.scan_while(is_opsymbol);
675 let tok = match run.as_str() {
676 "-" => Token::ExactMinus,
677 "--" => Token::PathLine,
678 "->" => Token::Arrow,
679 _ => Token::BinopMinus(run),
680 };
681 self.emit(start, tok);
682 return Ok(());
683 }
684 '*' => {
685 let run = self.scan_while(is_opsymbol);
686 let tok = if run == "*" {
687 Token::ExactTimes
688 } else {
689 Token::BinopTimes(run)
690 };
691 self.emit(start, tok);
692 return Ok(());
693 }
694 '/' => {
695 let run = self.scan_while(is_opsymbol);
696 self.emit(start, Token::BinopDivides(run));
697 return Ok(());
698 }
699 '=' => {
700 let run = self.scan_while(is_opsymbol);
701 let tok = if run == "=" {
702 Token::DefEq
703 } else {
704 Token::BinopEq(run)
705 };
706 self.emit(start, tok);
707 return Ok(());
708 }
709 '<' => {
710 if self.peek_at(1) == Some('[') {
711 self.bump_n(2);
712 self.emit(start, Token::BPath);
713 return Ok(());
714 }
715 let run = self.scan_while(is_opsymbol);
716 let tok = if run == "<-" {
717 Token::OverwriteEq
718 } else {
719 Token::BinopLt(run)
720 };
721 self.emit(start, tok);
722 return Ok(());
723 }
724 '>' => {
725 let run = self.scan_while(is_opsymbol);
726 self.emit(start, Token::BinopGt(run));
727 return Ok(());
728 }
729 '&' => {
730 let run = self.scan_while(is_opsymbol);
731 let tok = if run == "&" {
732 Token::ExactAmp
733 } else {
734 Token::BinopAmp(run)
735 };
736 self.emit(start, tok);
737 return Ok(());
738 }
739 '|' => {
740 if self.peek_at(1) == Some(')') {
741 self.bump_n(2);
742 self.pop_mode(start, "too many closing")?;
743 self.emit(start, Token::ERecord);
744 return Ok(());
745 }
746 let run = self.scan_while(is_opsymbol);
747 let tok = if run == "|" {
748 Token::Bar
749 } else {
750 Token::BinopBar(run)
751 };
752 self.emit(start, tok);
753 return Ok(());
754 }
755 '^' => {
756 let run = self.scan_while(is_opsymbol);
757 self.emit(start, Token::BinopHat(run));
758 return Ok(());
759 }
760 '!' => {
761 let run = self.scan_while(is_opsymbol);
762 self.emit(start, Token::UnopExclam(run));
763 return Ok(());
764 }
765 '~' => {
766 self.bump();
767 self.emit(start, Token::ExactTilde);
768 return Ok(());
769 }
770 ':' => {
771 self.bump();
772 if self.peek() == Some(':') {
773 self.bump();
774 self.emit(start, Token::Cons);
775 } else if self.version == RustyfiVersion::V0_1 && self.peek() == Some('>') {
776 self.bump();
783 self.emit(start, Token::Coerce);
784 } else {
785 self.emit(start, Token::Colon);
786 }
787 return Ok(());
788 }
789 '.' => {
790 if self.peek_at(1) == Some('.') {
791 self.bump_n(2);
792 self.emit(start, Token::PathCurve);
793 return Ok(());
794 }
795 if self.peek_at(1).is_some_and(is_digit) {
796 return self.lex_number(start);
797 }
798 return self.error(start, "illegal token '.' in a program area");
799 }
800 '?' => {
801 self.bump();
802 let tok = if self.version == RustyfiVersion::V0_1 {
823 if self.peek() == Some('\'') {
824 if let Some(len) = self.ident_len_at(1) {
825 self.bump(); let name = self.take(len);
827 Token::RowVar(name)
828 } else {
829 Token::OptionalType
830 }
831 } else {
832 Token::OptionalType
833 }
834 } else {
835 match self.peek() {
836 Some(':') => {
837 self.bump();
838 Token::Optional
839 }
840 Some('*') => {
841 self.bump();
842 Token::Omission
843 }
844 Some('-') if self.peek_at(1) == Some('>') => {
845 self.bump_n(2);
846 Token::OptionalArrow
847 }
848 _ => Token::OptionalType,
849 }
850 };
851 self.emit(start, tok);
852 return Ok(());
853 }
854 _ if is_digit(c) => {
855 return self.lex_number(start);
856 }
857 _ if is_small(c) => {
858 let len = self.name_len_at(0).unwrap();
859 let name = self.take(len);
860 let tok = self.keyword(&name).unwrap_or(Token::Var(name));
861 self.emit(start, tok);
862 return Ok(());
863 }
864 _ if is_capital(c) => {
865 let (mods, last, last_is_ctor) = self.scan_dotted().unwrap();
866 if mods.is_empty() && last_is_ctor {
867 if self.peek() == Some('.') && self.peek_at(1) == Some('(') {
869 self.bump_n(2);
870 self.push_mode(Mode::Program);
871 self.emit(start, Token::OpenModule(last));
872 } else {
873 self.emit(start, Token::Constructor(last));
874 }
875 } else if last_is_ctor {
876 if self.version == RustyfiVersion::V0_1 {
877 self.emit(start, Token::LongUpper(mods, last));
880 } else {
881 return self.error(start, "module path must end with a variable name");
884 }
885 } else {
886 self.emit(start, Token::VarWithMod(mods, last));
887 }
888 return Ok(());
889 }
890 _ => {
891 self.bump();
892 return self.error(start, format!("illegal token '{c}' in a program area"));
893 }
894 }
895 }
896 }
897
898 fn lex_header(&mut self, start: Loc) -> Result<(), LexError> {
900 if self.peek() == Some('`') {
901 return self.error(start, "positioned string literals '@`' are not supported yet");
902 }
903 let Some(len) = self.ident_len_at(0) else {
904 return self.error(start, "illegal token '@' in a program area");
905 };
906 let headertype = self.take(len);
907 if self.peek() != Some(':') {
908 return self.error(start, format!("undefined header type '{headertype}'"));
909 }
910 self.bump();
911 while self.peek() == Some(' ') {
912 self.bump();
913 }
914 let mut content = String::new();
915 while let Some(c) = self.peek() {
916 if is_break(c) {
917 self.bump();
918 if c == '\r' && self.peek() == Some('\n') {
928 self.bump();
929 }
930 break;
931 }
932 content.push(self.bump());
933 }
934 let tok = match headertype.as_str() {
935 "require" => Token::HeaderRequire(content),
936 "import" => Token::HeaderImport(content),
937 "stage" => {
938 if self.version == RustyfiVersion::V0_1 {
945 return self.error(
946 start,
947 "the '@stage:' header does not exist in SATySFi 0.1 \
948 (staging is per-binding there: 'val ~x' / 'val persistent ~x')",
949 );
950 }
951 match content.as_str() {
952 "persistent" => Token::HeaderPersistent0,
953 "0" => Token::HeaderStage0,
954 "1" => Token::HeaderStage1,
955 _ => {
956 return self.error(
957 start,
958 format!(
959 "undefined stage type '{content}'; should be 'persistent', '0', or '1'."
960 ),
961 )
962 }
963 }
964 }
965 _ => return self.error(start, format!("undefined header type '{headertype}'")),
966 };
967 self.emit(start, tok);
968 Ok(())
969 }
970
971 fn lex_number(&mut self, start: Loc) -> Result<(), LexError> {
973 if self.peek() == Some('0')
974 && matches!(self.peek_at(1), Some('x') | Some('X'))
975 && self.peek_at(2).is_some_and(is_hex)
976 {
977 self.bump_n(2);
978 let hex = self.scan_while(is_hex);
979 let value = i64::from_str_radix(&hex, 16).map_err(|_| LexError {
980 span: Span::new(start, self.loc()),
981 msg: format!("malformed hexadecimal constant '0x{hex}'"),
982 })?;
983 self.emit(start, Token::IntConst(value));
984 return Ok(());
985 }
986 if let Some((_, num_len, unit_len)) = self.length_lookahead() {
987 let num = self.take(num_len);
988 let unit = self.take(unit_len);
989 let value: f64 = num.parse().unwrap_or_default();
990 self.emit(start, Token::LengthConst(value, unit));
991 return Ok(());
992 }
993 let int_part = self.scan_while(is_digit);
994 if self.peek() == Some('.')
995 && (self.peek_at(1).is_some_and(is_digit) || !int_part.is_empty())
996 {
997 self.bump();
998 let frac = self.scan_while(is_digit);
999 let text = format!("{int_part}.{frac}");
1000 let value: f64 = text.parse().unwrap_or_default();
1001 self.emit(start, Token::FloatConst(value));
1002 } else {
1003 let value: i64 = int_part.parse().map_err(|_| LexError {
1004 span: Span::new(start, self.loc()),
1005 msg: format!("malformed integer constant '{int_part}'"),
1006 })?;
1007 self.emit(start, Token::IntConst(value));
1008 }
1009 Ok(())
1010 }
1011
1012 fn lex_vertical(&mut self) -> Result<(), LexError> {
1015 loop {
1016 let start = self.loc();
1017 let Some(c) = self.peek() else {
1018 if self.stack.len() == 1 {
1019 self.emit(start, Token::Eoi);
1020 return Ok(());
1021 }
1022 return self.error(start, "unexpected end of input while reading a vertical area");
1023 };
1024 match c {
1025 '%' => {
1026 self.bump();
1027 self.comment();
1028 }
1029 _ if is_space(c) || is_break(c) => {
1030 self.bump();
1031 }
1032 '#' => {
1033 self.bump();
1034 let Some((mods, name, _)) = self.scan_dotted() else {
1035 return self.error(start, "unexpected character '#' in a vertical area");
1036 };
1037 self.push_mode(Mode::Active);
1038 self.emit(start, Token::VarInVert(mods, name));
1039 return Ok(());
1040 }
1041 '+' => {
1042 self.bump();
1043 let Some((mods, name, _)) = self.scan_dotted() else {
1044 return self.error(start, "unexpected character '+' in a vertical area");
1045 };
1046 self.push_mode(Mode::Active);
1047 if mods.is_empty() {
1048 if self.peek() == Some('@') {
1049 self.bump();
1050 self.emit(start, Token::VertMacro(format!("+{name}@")));
1051 } else {
1052 self.emit(start, Token::VertCmd(format!("+{name}")));
1053 }
1054 } else {
1055 self.emit(start, Token::VertCmdWithMod(mods, format!("+{name}")));
1056 }
1057 return Ok(());
1058 }
1059 '<' => {
1060 self.bump();
1061 self.push_mode(Mode::Vertical);
1062 self.emit(start, Token::BVertGrp);
1063 return Ok(());
1064 }
1065 '>' => {
1066 self.bump();
1067 self.pop_mode(start, "too many closing")?;
1068 self.emit(start, Token::EVertGrp);
1069 return Ok(());
1070 }
1071 '{' => {
1072 self.bump();
1073 self.push_mode(Mode::Horizontal);
1074 self.skip_spaces();
1075 self.emit(start, Token::BHorzGrp);
1076 return Ok(());
1077 }
1078 _ => {
1079 self.bump();
1080 return self.error(
1081 start,
1082 format!("unexpected character '{c}' in a vertical area"),
1083 );
1084 }
1085 }
1086 }
1087 }
1088
1089 fn lex_horizontal(&mut self) -> Result<(), LexError> {
1092 loop {
1093 let start = self.loc();
1094 let Some(c) = self.peek() else {
1095 if self.stack.len() == 1 {
1096 self.emit(start, Token::Eoi);
1097 return Ok(());
1098 }
1099 return self.error(
1100 start,
1101 "unexpected end of input while reading an inline text area",
1102 );
1103 };
1104
1105 if c == '%' {
1106 self.bump();
1107 self.comment();
1108 self.skip_spaces();
1109 continue;
1110 }
1111
1112 let ws = self.count_at(0, |c| is_space(c) || is_break(c));
1114 match self.peek_at(ws) {
1115 Some('{') => {
1116 self.bump_n(ws + 1);
1117 self.push_mode(Mode::Horizontal);
1118 self.skip_spaces();
1119 self.emit(start, Token::BHorzGrp);
1120 return Ok(());
1121 }
1122 Some('}') => {
1123 self.bump_n(ws + 1);
1124 self.pop_mode(start, "too many closing")?;
1125 self.emit(start, Token::EHorzGrp);
1126 return Ok(());
1127 }
1128 Some('<') => {
1129 self.bump_n(ws + 1);
1130 self.push_mode(Mode::Vertical);
1131 self.emit(start, Token::BVertGrp);
1132 return Ok(());
1133 }
1134 Some('|') => {
1135 self.bump_n(ws + 1);
1136 self.skip_spaces();
1137 self.emit(start, Token::Sep);
1138 return Ok(());
1139 }
1140 Some('*') => {
1141 self.bump_n(ws);
1142 let stars = self.scan_while(|c| c == '*');
1143 self.skip_spaces();
1144 self.emit(start, Token::Item(stars.len()));
1145 return Ok(());
1146 }
1147 _ => {}
1148 }
1149 if ws > 0 {
1150 let first = self.peek().unwrap();
1151 self.bump_n(ws);
1152 self.skip_spaces();
1153 self.emit(start, if is_break(first) { Token::Break } else { Token::Space });
1154 return Ok(());
1155 }
1156
1157 match c {
1158 '#' => {
1159 self.bump();
1160 if self.peek() == Some('`') {
1161 let quotes = self.scan_while(|c| c == '`');
1162 self.literal_horz(start, quotes.len(), false)?;
1163 return Ok(());
1164 }
1165 let Some((mods, name, _)) = self.scan_dotted() else {
1166 return self.error(start, "illegal token '#' in an inline text area");
1167 };
1168 self.push_mode(Mode::Active);
1169 self.emit(start, Token::VarInHorz(mods, name));
1170 return Ok(());
1171 }
1172 '\\' => {
1173 self.bump();
1174 if let Some((mods, name, _)) = self.scan_dotted() {
1175 if mods.is_empty() {
1176 if self.peek() == Some('@') {
1177 self.bump();
1178 self.push_mode(Mode::Active);
1179 self.emit(start, Token::HorzMacro(format!("\\{name}@")));
1180 } else {
1181 self.push_mode(Mode::Active);
1182 self.emit(start, Token::HorzCmd(format!("\\{name}")));
1183 }
1184 } else {
1185 self.push_mode(Mode::Active);
1186 self.emit(start, Token::HorzCmdWithMod(mods, format!("\\{name}")));
1187 }
1188 return Ok(());
1189 }
1190 if self.peek().is_some_and(is_symbol_char) {
1191 let sym = self.bump();
1192 self.emit(start, Token::Char(sym.to_string()));
1193 return Ok(());
1194 }
1195 return self.error(start, "illegal token '\\' in an inline text area");
1196 }
1197 '$' => {
1198 self.bump();
1199 if self.peek() == Some('{') {
1200 self.bump();
1201 self.push_mode(Mode::Math);
1202 self.emit(start, Token::BMathGrp);
1203 return Ok(());
1204 }
1205 return self.error(start, "illegal token '$' in an inline text area");
1206 }
1207 '`' => {
1208 let quotes = self.scan_while(|c| c == '`');
1209 self.literal_horz(start, quotes.len(), true)?;
1210 return Ok(());
1211 }
1212 _ if is_str_char(c) => {
1213 let text = self.scan_while(is_str_char);
1214 self.emit(start, Token::Char(text));
1215 return Ok(());
1216 }
1217 _ => {
1218 self.bump();
1219 return self.error(
1220 start,
1221 format!("illegal token '{c}' in an inline text area"),
1222 );
1223 }
1224 }
1225 }
1226 }
1227
1228 fn lex_active(&mut self) -> Result<(), LexError> {
1231 loop {
1232 let start = self.loc();
1233 let Some(c) = self.peek() else {
1234 return self.error(start, "unexpected end of input while reading an active area");
1235 };
1236 match c {
1237 '%' => {
1238 self.bump();
1239 self.comment();
1240 }
1241 _ if is_space(c) || is_break(c) => {
1242 self.bump();
1243 }
1244 '?' => {
1245 self.bump();
1246 if self.version == RustyfiVersion::V0_1 {
1257 self.emit(start, Token::OptionalType);
1258 return Ok(());
1259 }
1260 match self.peek() {
1261 Some(':') => {
1262 self.bump();
1263 self.emit(start, Token::Optional);
1264 }
1265 Some('*') => {
1266 self.bump();
1267 self.emit(start, Token::Omission);
1268 }
1269 _ => return self.error(start, "unexpected token '?' in an active area"),
1270 }
1271 return Ok(());
1272 }
1273 '~' => {
1274 self.bump();
1275 self.emit(start, Token::ExactTilde);
1276 return Ok(());
1277 }
1278 '(' => {
1279 self.bump();
1280 if self.peek() == Some('|') {
1281 self.bump();
1282 self.push_mode(Mode::Program);
1283 self.emit(start, Token::BRecord);
1284 } else {
1285 self.push_mode(Mode::Program);
1286 self.emit(start, Token::LParen);
1287 }
1288 return Ok(());
1289 }
1290 '[' => {
1291 self.bump();
1292 self.push_mode(Mode::Program);
1293 self.emit(start, Token::BList);
1294 return Ok(());
1295 }
1296 '{' => {
1297 self.bump();
1298 self.pop_mode(start, "BUG; this cannot happen")?;
1299 self.push_mode(Mode::Horizontal);
1300 self.skip_spaces();
1301 self.emit(start, Token::BHorzGrp);
1302 return Ok(());
1303 }
1304 '<' => {
1305 self.bump();
1306 self.pop_mode(start, "BUG; this cannot happen")?;
1307 self.push_mode(Mode::Vertical);
1308 self.emit(start, Token::BVertGrp);
1309 return Ok(());
1310 }
1311 ';' => {
1312 self.bump();
1313 self.pop_mode(start, "BUG; this cannot happen")?;
1314 self.emit(start, Token::EndActive);
1315 return Ok(());
1316 }
1317 _ => {
1318 self.bump();
1319 return self.error(start, format!("unexpected token '{c}' in an active area"));
1320 }
1321 }
1322 }
1323 }
1324
1325 fn lex_math(&mut self) -> Result<(), LexError> {
1328 loop {
1329 let start = self.loc();
1330 let Some(c) = self.peek() else {
1331 return self.error(start, "unexpected end of file in a math area");
1332 };
1333 match c {
1334 '%' => {
1335 self.bump();
1336 self.comment();
1337 }
1338 _ if is_space(c) || is_break(c) => {
1339 self.bump();
1340 }
1341 '?' => {
1342 self.bump();
1343 match self.peek() {
1344 Some(':') => {
1345 self.bump();
1346 self.emit(start, Token::Optional);
1347 }
1348 Some('*') => {
1349 self.bump();
1350 self.emit(start, Token::Omission);
1351 }
1352 _ => return self.error(start, "illegal token '?' in a math area"),
1353 }
1354 return Ok(());
1355 }
1356 '!' => {
1357 self.bump();
1358 match self.peek() {
1359 Some('{') => {
1360 self.bump();
1361 self.push_mode(Mode::Horizontal);
1362 self.skip_spaces();
1363 self.emit(start, Token::BHorzGrp);
1364 }
1365 Some('<') => {
1366 self.bump();
1367 self.push_mode(Mode::Vertical);
1368 self.emit(start, Token::BVertGrp);
1369 }
1370 Some('(') => {
1371 self.bump();
1372 if self.peek() == Some('|') {
1373 self.bump();
1374 self.push_mode(Mode::Program);
1375 self.emit(start, Token::BRecord);
1376 } else {
1377 self.push_mode(Mode::Program);
1378 self.emit(start, Token::LParen);
1379 }
1380 }
1381 Some('[') => {
1382 self.bump();
1383 self.push_mode(Mode::Program);
1384 self.emit(start, Token::BList);
1385 }
1386 _ => return self.error(start, "illegal token '!' in a math area"),
1387 }
1388 return Ok(());
1389 }
1390 '{' => {
1391 self.bump();
1392 self.push_mode(Mode::Math);
1393 self.emit(start, Token::BMathGrp);
1394 return Ok(());
1395 }
1396 '}' => {
1397 self.bump();
1398 self.pop_mode(start, "too many closing")?;
1399 self.emit(start, Token::EMathGrp);
1400 return Ok(());
1401 }
1402 '|' => {
1403 self.bump();
1404 self.emit(start, Token::Sep);
1405 return Ok(());
1406 }
1407 '^' => {
1408 self.bump();
1409 self.emit(start, Token::Superscript);
1410 return Ok(());
1411 }
1412 '_' => {
1413 self.bump();
1414 self.emit(start, Token::Subscript);
1415 return Ok(());
1416 }
1417 '\'' => {
1418 let primes = self.scan_while(|c| c == '\'');
1419 self.emit(start, Token::Primes(primes.len()));
1420 return Ok(());
1421 }
1422 '#' => {
1423 self.bump();
1424 let Some((mods, name, _)) = self.scan_dotted() else {
1425 return self.error(start, "illegal token '#' in a math area");
1426 };
1427 self.emit(start, Token::VarInMath(mods, name));
1428 return Ok(());
1429 }
1430 '\\' => {
1431 self.bump();
1432 if let Some((mods, name, _)) = self.scan_dotted() {
1433 if mods.is_empty() {
1434 self.emit(start, Token::MathCmd(format!("\\{name}")));
1435 } else {
1436 self.emit(start, Token::MathCmdWithMod(mods, format!("\\{name}")));
1437 }
1438 return Ok(());
1439 }
1440 if self.peek().is_some_and(is_symbol_char) {
1441 let sym = self.bump();
1442 self.emit(start, Token::MathChar(sym.to_string()));
1443 return Ok(());
1444 }
1445 return self.error(start, "illegal token '\\' in a math area");
1446 }
1447 _ if is_mathsymbol_top(c) => {
1466 self.bump();
1467 self.emit(start, Token::MathChar(c.to_string()));
1468 return Ok(());
1469 }
1470 _ if c.is_ascii_alphanumeric() => {
1471 self.bump();
1472 self.emit(start, Token::MathChar(c.to_string()));
1473 return Ok(());
1474 }
1475 _ => {
1476 self.bump();
1477 return self.error(start, format!("illegal token '{c}' in a math area"));
1478 }
1479 }
1480 }
1481 }
1482}