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