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 break;
919 }
920 content.push(self.bump());
921 }
922 let tok = match headertype.as_str() {
923 "require" => Token::HeaderRequire(content),
924 "import" => Token::HeaderImport(content),
925 "stage" => {
926 if self.version == RustyfiVersion::V0_1 {
933 return self.error(
934 start,
935 "the '@stage:' header does not exist in SATySFi 0.1 \
936 (staging is per-binding there: 'val ~x' / 'val persistent ~x')",
937 );
938 }
939 match content.as_str() {
940 "persistent" => Token::HeaderPersistent0,
941 "0" => Token::HeaderStage0,
942 "1" => Token::HeaderStage1,
943 _ => {
944 return self.error(
945 start,
946 format!(
947 "undefined stage type '{content}'; should be 'persistent', '0', or '1'."
948 ),
949 )
950 }
951 }
952 }
953 _ => return self.error(start, format!("undefined header type '{headertype}'")),
954 };
955 self.emit(start, tok);
956 Ok(())
957 }
958
959 fn lex_number(&mut self, start: Loc) -> Result<(), LexError> {
961 if self.peek() == Some('0')
962 && matches!(self.peek_at(1), Some('x') | Some('X'))
963 && self.peek_at(2).is_some_and(is_hex)
964 {
965 self.bump_n(2);
966 let hex = self.scan_while(is_hex);
967 let value = i64::from_str_radix(&hex, 16).map_err(|_| LexError {
968 span: Span::new(start, self.loc()),
969 msg: format!("malformed hexadecimal constant '0x{hex}'"),
970 })?;
971 self.emit(start, Token::IntConst(value));
972 return Ok(());
973 }
974 if let Some((_, num_len, unit_len)) = self.length_lookahead() {
975 let num = self.take(num_len);
976 let unit = self.take(unit_len);
977 let value: f64 = num.parse().unwrap_or_default();
978 self.emit(start, Token::LengthConst(value, unit));
979 return Ok(());
980 }
981 let int_part = self.scan_while(is_digit);
982 if self.peek() == Some('.')
983 && (self.peek_at(1).is_some_and(is_digit) || !int_part.is_empty())
984 {
985 self.bump();
986 let frac = self.scan_while(is_digit);
987 let text = format!("{int_part}.{frac}");
988 let value: f64 = text.parse().unwrap_or_default();
989 self.emit(start, Token::FloatConst(value));
990 } else {
991 let value: i64 = int_part.parse().map_err(|_| LexError {
992 span: Span::new(start, self.loc()),
993 msg: format!("malformed integer constant '{int_part}'"),
994 })?;
995 self.emit(start, Token::IntConst(value));
996 }
997 Ok(())
998 }
999
1000 fn lex_vertical(&mut self) -> Result<(), LexError> {
1003 loop {
1004 let start = self.loc();
1005 let Some(c) = self.peek() else {
1006 if self.stack.len() == 1 {
1007 self.emit(start, Token::Eoi);
1008 return Ok(());
1009 }
1010 return self.error(start, "unexpected end of input while reading a vertical area");
1011 };
1012 match c {
1013 '%' => {
1014 self.bump();
1015 self.comment();
1016 }
1017 _ if is_space(c) || is_break(c) => {
1018 self.bump();
1019 }
1020 '#' => {
1021 self.bump();
1022 let Some((mods, name, _)) = self.scan_dotted() else {
1023 return self.error(start, "unexpected character '#' in a vertical area");
1024 };
1025 self.push_mode(Mode::Active);
1026 self.emit(start, Token::VarInVert(mods, name));
1027 return Ok(());
1028 }
1029 '+' => {
1030 self.bump();
1031 let Some((mods, name, _)) = self.scan_dotted() else {
1032 return self.error(start, "unexpected character '+' in a vertical area");
1033 };
1034 self.push_mode(Mode::Active);
1035 if mods.is_empty() {
1036 if self.peek() == Some('@') {
1037 self.bump();
1038 self.emit(start, Token::VertMacro(format!("+{name}@")));
1039 } else {
1040 self.emit(start, Token::VertCmd(format!("+{name}")));
1041 }
1042 } else {
1043 self.emit(start, Token::VertCmdWithMod(mods, format!("+{name}")));
1044 }
1045 return Ok(());
1046 }
1047 '<' => {
1048 self.bump();
1049 self.push_mode(Mode::Vertical);
1050 self.emit(start, Token::BVertGrp);
1051 return Ok(());
1052 }
1053 '>' => {
1054 self.bump();
1055 self.pop_mode(start, "too many closing")?;
1056 self.emit(start, Token::EVertGrp);
1057 return Ok(());
1058 }
1059 '{' => {
1060 self.bump();
1061 self.push_mode(Mode::Horizontal);
1062 self.skip_spaces();
1063 self.emit(start, Token::BHorzGrp);
1064 return Ok(());
1065 }
1066 _ => {
1067 self.bump();
1068 return self.error(
1069 start,
1070 format!("unexpected character '{c}' in a vertical area"),
1071 );
1072 }
1073 }
1074 }
1075 }
1076
1077 fn lex_horizontal(&mut self) -> Result<(), LexError> {
1080 loop {
1081 let start = self.loc();
1082 let Some(c) = self.peek() else {
1083 if self.stack.len() == 1 {
1084 self.emit(start, Token::Eoi);
1085 return Ok(());
1086 }
1087 return self.error(
1088 start,
1089 "unexpected end of input while reading an inline text area",
1090 );
1091 };
1092
1093 if c == '%' {
1094 self.bump();
1095 self.comment();
1096 self.skip_spaces();
1097 continue;
1098 }
1099
1100 let ws = self.count_at(0, |c| is_space(c) || is_break(c));
1102 match self.peek_at(ws) {
1103 Some('{') => {
1104 self.bump_n(ws + 1);
1105 self.push_mode(Mode::Horizontal);
1106 self.skip_spaces();
1107 self.emit(start, Token::BHorzGrp);
1108 return Ok(());
1109 }
1110 Some('}') => {
1111 self.bump_n(ws + 1);
1112 self.pop_mode(start, "too many closing")?;
1113 self.emit(start, Token::EHorzGrp);
1114 return Ok(());
1115 }
1116 Some('<') => {
1117 self.bump_n(ws + 1);
1118 self.push_mode(Mode::Vertical);
1119 self.emit(start, Token::BVertGrp);
1120 return Ok(());
1121 }
1122 Some('|') => {
1123 self.bump_n(ws + 1);
1124 self.skip_spaces();
1125 self.emit(start, Token::Sep);
1126 return Ok(());
1127 }
1128 Some('*') => {
1129 self.bump_n(ws);
1130 let stars = self.scan_while(|c| c == '*');
1131 self.skip_spaces();
1132 self.emit(start, Token::Item(stars.len()));
1133 return Ok(());
1134 }
1135 _ => {}
1136 }
1137 if ws > 0 {
1138 let first = self.peek().unwrap();
1139 self.bump_n(ws);
1140 self.skip_spaces();
1141 self.emit(start, if is_break(first) { Token::Break } else { Token::Space });
1142 return Ok(());
1143 }
1144
1145 match c {
1146 '#' => {
1147 self.bump();
1148 if self.peek() == Some('`') {
1149 let quotes = self.scan_while(|c| c == '`');
1150 self.literal_horz(start, quotes.len(), false)?;
1151 return Ok(());
1152 }
1153 let Some((mods, name, _)) = self.scan_dotted() else {
1154 return self.error(start, "illegal token '#' in an inline text area");
1155 };
1156 self.push_mode(Mode::Active);
1157 self.emit(start, Token::VarInHorz(mods, name));
1158 return Ok(());
1159 }
1160 '\\' => {
1161 self.bump();
1162 if let Some((mods, name, _)) = self.scan_dotted() {
1163 if mods.is_empty() {
1164 if self.peek() == Some('@') {
1165 self.bump();
1166 self.push_mode(Mode::Active);
1167 self.emit(start, Token::HorzMacro(format!("\\{name}@")));
1168 } else {
1169 self.push_mode(Mode::Active);
1170 self.emit(start, Token::HorzCmd(format!("\\{name}")));
1171 }
1172 } else {
1173 self.push_mode(Mode::Active);
1174 self.emit(start, Token::HorzCmdWithMod(mods, format!("\\{name}")));
1175 }
1176 return Ok(());
1177 }
1178 if self.peek().is_some_and(is_symbol_char) {
1179 let sym = self.bump();
1180 self.emit(start, Token::Char(sym.to_string()));
1181 return Ok(());
1182 }
1183 return self.error(start, "illegal token '\\' in an inline text area");
1184 }
1185 '$' => {
1186 self.bump();
1187 if self.peek() == Some('{') {
1188 self.bump();
1189 self.push_mode(Mode::Math);
1190 self.emit(start, Token::BMathGrp);
1191 return Ok(());
1192 }
1193 return self.error(start, "illegal token '$' in an inline text area");
1194 }
1195 '`' => {
1196 let quotes = self.scan_while(|c| c == '`');
1197 self.literal_horz(start, quotes.len(), true)?;
1198 return Ok(());
1199 }
1200 _ if is_str_char(c) => {
1201 let text = self.scan_while(is_str_char);
1202 self.emit(start, Token::Char(text));
1203 return Ok(());
1204 }
1205 _ => {
1206 self.bump();
1207 return self.error(
1208 start,
1209 format!("illegal token '{c}' in an inline text area"),
1210 );
1211 }
1212 }
1213 }
1214 }
1215
1216 fn lex_active(&mut self) -> Result<(), LexError> {
1219 loop {
1220 let start = self.loc();
1221 let Some(c) = self.peek() else {
1222 return self.error(start, "unexpected end of input while reading an active area");
1223 };
1224 match c {
1225 '%' => {
1226 self.bump();
1227 self.comment();
1228 }
1229 _ if is_space(c) || is_break(c) => {
1230 self.bump();
1231 }
1232 '?' => {
1233 self.bump();
1234 if self.version == RustyfiVersion::V0_1 {
1245 self.emit(start, Token::OptionalType);
1246 return Ok(());
1247 }
1248 match self.peek() {
1249 Some(':') => {
1250 self.bump();
1251 self.emit(start, Token::Optional);
1252 }
1253 Some('*') => {
1254 self.bump();
1255 self.emit(start, Token::Omission);
1256 }
1257 _ => return self.error(start, "unexpected token '?' in an active area"),
1258 }
1259 return Ok(());
1260 }
1261 '~' => {
1262 self.bump();
1263 self.emit(start, Token::ExactTilde);
1264 return Ok(());
1265 }
1266 '(' => {
1267 self.bump();
1268 if self.peek() == Some('|') {
1269 self.bump();
1270 self.push_mode(Mode::Program);
1271 self.emit(start, Token::BRecord);
1272 } else {
1273 self.push_mode(Mode::Program);
1274 self.emit(start, Token::LParen);
1275 }
1276 return Ok(());
1277 }
1278 '[' => {
1279 self.bump();
1280 self.push_mode(Mode::Program);
1281 self.emit(start, Token::BList);
1282 return Ok(());
1283 }
1284 '{' => {
1285 self.bump();
1286 self.pop_mode(start, "BUG; this cannot happen")?;
1287 self.push_mode(Mode::Horizontal);
1288 self.skip_spaces();
1289 self.emit(start, Token::BHorzGrp);
1290 return Ok(());
1291 }
1292 '<' => {
1293 self.bump();
1294 self.pop_mode(start, "BUG; this cannot happen")?;
1295 self.push_mode(Mode::Vertical);
1296 self.emit(start, Token::BVertGrp);
1297 return Ok(());
1298 }
1299 ';' => {
1300 self.bump();
1301 self.pop_mode(start, "BUG; this cannot happen")?;
1302 self.emit(start, Token::EndActive);
1303 return Ok(());
1304 }
1305 _ => {
1306 self.bump();
1307 return self.error(start, format!("unexpected token '{c}' in an active area"));
1308 }
1309 }
1310 }
1311 }
1312
1313 fn lex_math(&mut self) -> Result<(), LexError> {
1316 loop {
1317 let start = self.loc();
1318 let Some(c) = self.peek() else {
1319 return self.error(start, "unexpected end of file in a math area");
1320 };
1321 match c {
1322 '%' => {
1323 self.bump();
1324 self.comment();
1325 }
1326 _ if is_space(c) || is_break(c) => {
1327 self.bump();
1328 }
1329 '?' => {
1330 self.bump();
1331 match self.peek() {
1332 Some(':') => {
1333 self.bump();
1334 self.emit(start, Token::Optional);
1335 }
1336 Some('*') => {
1337 self.bump();
1338 self.emit(start, Token::Omission);
1339 }
1340 _ => return self.error(start, "illegal token '?' in a math area"),
1341 }
1342 return Ok(());
1343 }
1344 '!' => {
1345 self.bump();
1346 match self.peek() {
1347 Some('{') => {
1348 self.bump();
1349 self.push_mode(Mode::Horizontal);
1350 self.skip_spaces();
1351 self.emit(start, Token::BHorzGrp);
1352 }
1353 Some('<') => {
1354 self.bump();
1355 self.push_mode(Mode::Vertical);
1356 self.emit(start, Token::BVertGrp);
1357 }
1358 Some('(') => {
1359 self.bump();
1360 if self.peek() == Some('|') {
1361 self.bump();
1362 self.push_mode(Mode::Program);
1363 self.emit(start, Token::BRecord);
1364 } else {
1365 self.push_mode(Mode::Program);
1366 self.emit(start, Token::LParen);
1367 }
1368 }
1369 Some('[') => {
1370 self.bump();
1371 self.push_mode(Mode::Program);
1372 self.emit(start, Token::BList);
1373 }
1374 _ => return self.error(start, "illegal token '!' in a math area"),
1375 }
1376 return Ok(());
1377 }
1378 '{' => {
1379 self.bump();
1380 self.push_mode(Mode::Math);
1381 self.emit(start, Token::BMathGrp);
1382 return Ok(());
1383 }
1384 '}' => {
1385 self.bump();
1386 self.pop_mode(start, "too many closing")?;
1387 self.emit(start, Token::EMathGrp);
1388 return Ok(());
1389 }
1390 '|' => {
1391 self.bump();
1392 self.emit(start, Token::Sep);
1393 return Ok(());
1394 }
1395 '^' => {
1396 self.bump();
1397 self.emit(start, Token::Superscript);
1398 return Ok(());
1399 }
1400 '_' => {
1401 self.bump();
1402 self.emit(start, Token::Subscript);
1403 return Ok(());
1404 }
1405 '\'' => {
1406 let primes = self.scan_while(|c| c == '\'');
1407 self.emit(start, Token::Primes(primes.len()));
1408 return Ok(());
1409 }
1410 '#' => {
1411 self.bump();
1412 let Some((mods, name, _)) = self.scan_dotted() else {
1413 return self.error(start, "illegal token '#' in a math area");
1414 };
1415 self.emit(start, Token::VarInMath(mods, name));
1416 return Ok(());
1417 }
1418 '\\' => {
1419 self.bump();
1420 if let Some((mods, name, _)) = self.scan_dotted() {
1421 if mods.is_empty() {
1422 self.emit(start, Token::MathCmd(format!("\\{name}")));
1423 } else {
1424 self.emit(start, Token::MathCmdWithMod(mods, format!("\\{name}")));
1425 }
1426 return Ok(());
1427 }
1428 if self.peek().is_some_and(is_symbol_char) {
1429 let sym = self.bump();
1430 self.emit(start, Token::MathChar(sym.to_string()));
1431 return Ok(());
1432 }
1433 return self.error(start, "illegal token '\\' in a math area");
1434 }
1435 _ if is_mathsymbol_top(c) => {
1454 self.bump();
1455 self.emit(start, Token::MathChar(c.to_string()));
1456 return Ok(());
1457 }
1458 _ if c.is_ascii_alphanumeric() => {
1459 self.bump();
1460 self.emit(start, Token::MathChar(c.to_string()));
1461 return Ok(());
1462 }
1463 _ => {
1464 self.bump();
1465 return self.error(start, format!("illegal token '{c}' in a math area"));
1466 }
1467 }
1468 }
1469 }
1470}