Skip to main content

daml_parser/
lexer.rs

1//! DAML lexer: source text → tokens with spans.
2//!
3//! First stage of the real parser pipeline (lexer → layout → parse). Comments
4//! (line `--`, nested block `{- -}`) and string/char literals are resolved
5//! here, so no later stage can ever mistake `-- exercise the option` for a
6//! ledger action.
7
8/// 1-based source position of a token's first character.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Pos {
11    pub line: usize,
12    pub column: usize,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum TokenKind {
18    /// Lowercase-initial identifier, possibly qualified: `foo`, `Map.lookup`.
19    LowerId {
20        qualifier: Option<String>,
21        name: String,
22    },
23    /// Uppercase-initial identifier, possibly qualified: `Foo`, `DA.Set.Set`.
24    UpperId {
25        qualifier: Option<String>,
26        name: String,
27    },
28    /// Symbolic operator: `+`, `<-`, `->`, `=`, `=>`, `::`, `.`, `\`, ...
29    Op(String),
30    IntLit(String),
31    DecimalLit(String),
32    StringLit(String),
33    CharLit(String),
34    LParen,
35    RParen,
36    LBracket,
37    RBracket,
38    LBrace,
39    RBrace,
40    Comma,
41    Semi,
42    Backtick,
43    /// Layout-inserted virtual open brace (block start).
44    VLBrace,
45    /// Layout-inserted virtual close brace (block end).
46    VRBrace,
47    /// Layout-inserted virtual semicolon (new item at block indentation).
48    VSemi,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Token {
53    pub(crate) kind: TokenKind,
54    pub(crate) pos: Pos,
55    /// Byte offset of the token's first character in the source.
56    /// Virtual layout tokens are zero-width (`start == end`).
57    pub(crate) start: usize,
58    /// Byte offset one past the token's last character.
59    pub(crate) end: usize,
60}
61
62impl Token {
63    pub const fn kind(&self) -> &TokenKind {
64        &self.kind
65    }
66
67    pub const fn pos(&self) -> Pos {
68        self.pos
69    }
70
71    pub const fn start(&self) -> usize {
72        self.start
73    }
74
75    pub const fn end(&self) -> usize {
76        self.end
77    }
78
79    /// Layout-inserted tokens carry no source bytes (they are zero-width);
80    /// AST node-span computation skips them so spans tile the real source.
81    pub const fn is_virtual(&self) -> bool {
82        matches!(
83            self.kind,
84            TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
85        )
86    }
87}
88
89/// Source text the lexer consumes but the parser never sees.
90///
91/// Carries exact byte spans so a printer can re-attach comments to nearby AST
92/// nodes (which already have positions) and reproduce the original bytes.
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum TriviaKind {
96    /// `-- ...` to end of line (newline not included).
97    LineComment,
98    /// `{- ... -}`, possibly nested; unterminated runs to EOF.
99    BlockComment,
100    /// `#ifdef`/`#endif`/... preprocessor line at column 1.
101    CppDirective,
102    /// A run of N whitespace-only lines between tokens/comments.
103    BlankLines(usize),
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Trivia {
108    pub(crate) kind: TriviaKind,
109    /// Exact source slice (delimiters included; empty for `BlankLines`).
110    pub(crate) text: String,
111    pub(crate) pos: Pos,
112    pub(crate) start: usize,
113    pub(crate) end: usize,
114}
115
116impl Trivia {
117    pub const fn kind(&self) -> &TriviaKind {
118        &self.kind
119    }
120
121    pub fn text(&self) -> &str {
122        &self.text
123    }
124
125    pub const fn pos(&self) -> Pos {
126        self.pos
127    }
128
129    pub const fn start(&self) -> usize {
130        self.start
131    }
132
133    pub const fn end(&self) -> usize {
134        self.end
135    }
136}
137
138/// A lexical error. The scan must survive these: the caller reports the
139/// diagnostic and works with the tokens produced so far.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct LexError {
142    pub kind: LexErrorKind,
143    pub pos: Pos,
144}
145
146impl std::fmt::Display for LexError {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        self.kind.fmt(f)
149    }
150}
151
152impl std::error::Error for LexError {}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum LexErrorKind {
157    UnexpectedCharacter(char),
158    UnterminatedBlockComment,
159    UnterminatedStringLiteral,
160    UnterminatedStringGap,
161    InvalidEscapeSequence(char),
162    StraySingleQuote,
163    CharacterLiteralWrongLength,
164    HexLiteralMissingDigits,
165    DecimalExponentMissingDigits,
166}
167
168impl std::fmt::Display for LexErrorKind {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        match self {
171            Self::UnexpectedCharacter(c) => write!(f, "unexpected character '{c}'"),
172            Self::UnterminatedBlockComment => f.write_str("unterminated block comment"),
173            Self::UnterminatedStringLiteral => f.write_str("unterminated string literal"),
174            Self::UnterminatedStringGap => f.write_str("unterminated string gap"),
175            Self::InvalidEscapeSequence(c) => write!(f, "invalid escape sequence \\{c}"),
176            Self::StraySingleQuote => f.write_str("stray single quote"),
177            Self::CharacterLiteralWrongLength => {
178                f.write_str("character literal must contain exactly one character")
179            }
180            Self::HexLiteralMissingDigits => f.write_str("hex literal requires at least one digit"),
181            Self::DecimalExponentMissingDigits => {
182                f.write_str("decimal exponent requires at least one digit")
183            }
184        }
185    }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct LexOutput {
190    pub tokens: Vec<Token>,
191    pub errors: Vec<LexError>,
192}
193
194impl LexOutput {
195    pub fn into_parts(self) -> (Vec<Token>, Vec<LexError>) {
196        (self.tokens, self.errors)
197    }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct LexWithTriviaOutput {
202    pub tokens: Vec<Token>,
203    pub trivia: Vec<Trivia>,
204    pub errors: Vec<LexError>,
205}
206
207impl LexWithTriviaOutput {
208    pub fn into_parts(self) -> (Vec<Token>, Vec<Trivia>, Vec<LexError>) {
209        (self.tokens, self.trivia, self.errors)
210    }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214#[non_exhaustive]
215pub enum RenderLosslessError {
216    OverlappingSpans {
217        start: usize,
218    },
219    UncoveredBytes {
220        start: usize,
221        end: usize,
222        text: String,
223    },
224    UncoveredTail {
225        start: usize,
226        text: String,
227    },
228}
229
230impl std::fmt::Display for RenderLosslessError {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        match self {
233            Self::OverlappingSpans { start } => write!(f, "overlapping spans at byte {start}"),
234            Self::UncoveredBytes { start, end, text } => write!(
235                f,
236                "bytes {start}..{end} lost (not covered by any token/trivia): {text:?}"
237            ),
238            Self::UncoveredTail { start, text } => {
239                write!(f, "bytes {start}.. lost at EOF: {text:?}")
240            }
241        }
242    }
243}
244
245impl std::error::Error for RenderLosslessError {}
246
247const fn is_symbol_char(c: char) -> bool {
248    matches!(
249        c,
250        '!' | '#'
251            | '$'
252            | '%'
253            | '&'
254            | '*'
255            | '+'
256            | '.'
257            | '/'
258            | '<'
259            | '='
260            | '>'
261            | '?'
262            | '@'
263            | '\\'
264            | '^'
265            | '|'
266            | '-'
267            | '~'
268            | ':'
269    )
270}
271
272fn is_ident_start(c: char) -> bool {
273    c.is_alphabetic() || c == '_'
274}
275
276fn is_ident_char(c: char) -> bool {
277    c.is_alphanumeric() || c == '_' || c == '\''
278}
279
280pub(crate) const TAB_STOP: usize = 8;
281
282struct Lexer<'a> {
283    chars: Vec<char>,
284    src: &'a str,
285    i: usize,
286    byte: usize,
287    line: usize,
288    column: usize,
289    tokens: Vec<Token>,
290    trivia: Vec<Trivia>,
291    errors: Vec<LexError>,
292}
293
294pub fn lex(source: &str) -> LexOutput {
295    let LexWithTriviaOutput { tokens, errors, .. } = lex_with_trivia(source);
296    LexOutput { tokens, errors }
297}
298
299pub fn lex_with_trivia(source: &str) -> LexWithTriviaOutput {
300    let mut lexer = Lexer {
301        chars: source.chars().collect(),
302        src: source,
303        i: 0,
304        byte: 0,
305        line: 1,
306        column: 1,
307        tokens: Vec::new(),
308        trivia: Vec::new(),
309        errors: Vec::new(),
310    };
311    lexer.scan_tokens();
312    let mut trivia = lexer.trivia;
313    add_blank_line_trivia(source, &lexer.tokens, &mut trivia);
314    trivia.sort_by_key(|t| t.start);
315    LexWithTriviaOutput {
316        tokens: lexer.tokens,
317        trivia,
318        errors: lexer.errors,
319    }
320}
321
322/// Reconstruct the source from token and trivia spans.
323///
324/// `Ok` only when the spans tile the file — every non-whitespace byte inside
325/// exactly one token or comment span — in which case the result is
326/// byte-identical to `source`. This is the lossless-trivia oracle for the
327/// formatter.
328pub fn render_lossless(
329    source: &str,
330    tokens: &[Token],
331    trivia: &[Trivia],
332) -> Result<String, RenderLosslessError> {
333    let mut items: Vec<(usize, usize)> = tokens
334        .iter()
335        .filter(|t| {
336            !matches!(
337                t.kind,
338                TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
339            )
340        })
341        .map(|t| (t.start, t.end))
342        .chain(
343            trivia
344                .iter()
345                .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
346                .map(|t| (t.start, t.end)),
347        )
348        .collect();
349    items.sort_unstable();
350    let mut out = String::with_capacity(source.len());
351    let mut prev = 0usize;
352    for (start, end) in items {
353        if start < prev {
354            return Err(RenderLosslessError::OverlappingSpans { start });
355        }
356        let gap = &source[prev..start];
357        if !gap.chars().all(char::is_whitespace) {
358            return Err(RenderLosslessError::UncoveredBytes {
359                start: prev,
360                end: start,
361                text: gap.to_string(),
362            });
363        }
364        out.push_str(gap);
365        out.push_str(&source[start..end]);
366        prev = end;
367    }
368    let tail = &source[prev..];
369    if !tail.chars().all(char::is_whitespace) {
370        return Err(RenderLosslessError::UncoveredTail {
371            start: prev,
372            text: tail.to_string(),
373        });
374    }
375    out.push_str(tail);
376    Ok(out)
377}
378
379/// A blank line is a whitespace-only line lying entirely between spans (so a
380/// blank-looking line inside a block comment or multiline string does not
381/// count). Emitted as one `BlankLines(n)` per maximal run so the printer can
382/// preserve paragraph breaks.
383fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
384    let mut spans: Vec<(usize, usize)> = tokens
385        .iter()
386        .map(|t| (t.start, t.end))
387        .chain(trivia.iter().map(|t| (t.start, t.end)))
388        .collect();
389    spans.sort_unstable();
390    let bytes = source.as_bytes();
391    let mut blanks = Vec::new();
392    let mut gap_start = 0usize;
393    let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
394        // Newline offsets inside the gap. Full lines between two newlines
395        // (or before the first newline when the gap starts the file) are
396        // blank by construction: the gap holds no token/comment bytes, and
397        // any stray non-whitespace there fails the lossless render anyway.
398        let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
399        // Interior gap: the partial line before the first newline belongs to
400        // the preceding token's line, so only lines between newlines count.
401        // A gap at byte 0 has no such partial line — line 1 itself is blank.
402        let count = if from == 0 {
403            newlines.len()
404        } else {
405            newlines.len().saturating_sub(1)
406        };
407        if count == 0 {
408            return;
409        }
410        let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
411        let region_end = newlines[newlines.len() - 1] + 1;
412        let line = source[..region_start].matches('\n').count() + 1;
413        out.push(Trivia {
414            kind: TriviaKind::BlankLines(count),
415            text: String::new(),
416            pos: Pos { line, column: 1 },
417            start: region_start,
418            end: region_end,
419        });
420    };
421    for &(s, e) in &spans {
422        if s > gap_start {
423            emit_gap(gap_start, s, &mut blanks);
424        }
425        gap_start = gap_start.max(e);
426    }
427    if source.len() > gap_start {
428        emit_gap(gap_start, source.len(), &mut blanks);
429    }
430    trivia.extend(blanks);
431}
432
433impl<'a> Lexer<'a> {
434    fn peek(&self) -> Option<char> {
435        self.chars.get(self.i).copied()
436    }
437
438    fn peek_at(&self, n: usize) -> Option<char> {
439        self.chars.get(self.i + n).copied()
440    }
441
442    fn bump(&mut self) -> Option<char> {
443        let c = self.chars.get(self.i).copied()?;
444        self.i += 1;
445        self.byte += c.len_utf8();
446        match c {
447            '\n' => {
448                self.line += 1;
449                self.column = 1;
450            }
451            '\t' => {
452                // Tab advances to the next multiple-of-8 stop, matching GHC,
453                // so mixed tabs/spaces don't silently corrupt layout.
454                self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
455            }
456            _ => self.column += 1,
457        }
458        Some(c)
459    }
460
461    const fn pos(&self) -> Pos {
462        Pos {
463            line: self.line,
464            column: self.column,
465        }
466    }
467
468    /// `start` is the token's first byte; its end is wherever the cursor is
469    /// now, so call this immediately after consuming the token.
470    fn push(&mut self, tok: TokenKind, pos: Pos, start: usize) {
471        self.tokens.push(Token {
472            kind: tok,
473            pos,
474            start,
475            end: self.byte,
476        });
477    }
478
479    fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
480        self.trivia.push(Trivia {
481            kind,
482            text: self.src[start..self.byte].to_string(),
483            pos,
484            start,
485            end: self.byte,
486        });
487    }
488
489    fn error(&mut self, kind: LexErrorKind, pos: Pos) {
490        self.errors.push(LexError { kind, pos });
491    }
492
493    fn scan_tokens(&mut self) {
494        while let Some(c) = self.peek() {
495            let pos = self.pos();
496            let start = self.byte;
497            match c {
498                ' ' | '\t' | '\n' | '\r' => {
499                    self.bump();
500                }
501                '(' => {
502                    self.bump();
503                    self.push(TokenKind::LParen, pos, start);
504                }
505                ')' => {
506                    self.bump();
507                    self.push(TokenKind::RParen, pos, start);
508                }
509                '[' => {
510                    self.bump();
511                    self.push(TokenKind::LBracket, pos, start);
512                }
513                ']' => {
514                    self.bump();
515                    self.push(TokenKind::RBracket, pos, start);
516                }
517                ',' => {
518                    self.bump();
519                    self.push(TokenKind::Comma, pos, start);
520                }
521                ';' => {
522                    self.bump();
523                    self.push(TokenKind::Semi, pos, start);
524                }
525                '`' => {
526                    self.bump();
527                    self.push(TokenKind::Backtick, pos, start);
528                }
529                '{' => {
530                    if self.peek_at(1) == Some('-') {
531                        self.block_comment(pos);
532                    } else {
533                        self.bump();
534                        self.push(TokenKind::LBrace, pos, start);
535                    }
536                }
537                '}' => {
538                    self.bump();
539                    self.push(TokenKind::RBrace, pos, start);
540                }
541                // CPP preprocessor directive (#ifdef/#endif/#include...) at
542                // column 1 — daml-prim/stdlib sources use {-# LANGUAGE CPP #-};
543                // directives are line-based, skip the whole line.
544                '#' if self.column == 1
545                    && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
546                {
547                    while self.peek().is_some_and(|c| c != '\n') {
548                        self.bump();
549                    }
550                    self.push_trivia(TriviaKind::CppDirective, pos, start);
551                }
552                '"' => self.string_lit(pos),
553                '\'' => self.char_lit(pos),
554                c if c.is_ascii_digit() => self.number(pos),
555                c if is_ident_start(c) => self.identifier(pos),
556                c if is_symbol_char(c) => self.operator(pos),
557                _ => {
558                    self.bump();
559                    self.error(LexErrorKind::UnexpectedCharacter(c), pos);
560                }
561            }
562        }
563    }
564
565    /// `{- ... -}`, nested as in Haskell. Unterminated comment is an error
566    /// but consumes to EOF (no hang, no panic).
567    fn block_comment(&mut self, pos: Pos) {
568        let start = self.byte;
569        self.bump(); // {
570        self.bump(); // -
571        let mut depth = 1usize;
572        while depth > 0 {
573            match self.peek() {
574                None => {
575                    self.error(LexErrorKind::UnterminatedBlockComment, pos);
576                    self.push_trivia(TriviaKind::BlockComment, pos, start);
577                    return;
578                }
579                Some('{') if self.peek_at(1) == Some('-') => {
580                    self.bump();
581                    self.bump();
582                    depth += 1;
583                }
584                Some('-') if self.peek_at(1) == Some('}') => {
585                    self.bump();
586                    self.bump();
587                    depth -= 1;
588                }
589                Some(_) => {
590                    self.bump();
591                }
592            }
593        }
594        self.push_trivia(TriviaKind::BlockComment, pos, start);
595    }
596
597    fn string_lit(&mut self, pos: Pos) {
598        let start = self.byte;
599        self.bump(); // opening "
600        let mut value = String::new();
601        loop {
602            match self.peek() {
603                None | Some('\n') => {
604                    self.error(LexErrorKind::UnterminatedStringLiteral, pos);
605                    break;
606                }
607                Some('"') => {
608                    self.bump();
609                    break;
610                }
611                Some('\\') => {
612                    let escape_pos = self.pos();
613                    self.bump();
614                    match self.peek() {
615                        // String gap: backslash, whitespace, backslash.
616                        Some(w) if w.is_whitespace() => {
617                            while self.peek().is_some_and(|c| c.is_whitespace()) {
618                                self.bump();
619                            }
620                            if self.peek() == Some('\\') {
621                                self.bump();
622                            } else {
623                                self.error(LexErrorKind::UnterminatedStringGap, pos);
624                                break;
625                            }
626                        }
627                        Some(e) => {
628                            self.bump();
629                            match unescape(e) {
630                                Some(c) => value.push(c),
631                                None => {
632                                    self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
633                                    value.push(e);
634                                }
635                            }
636                        }
637                        None => {
638                            self.error(LexErrorKind::UnterminatedStringLiteral, pos);
639                            break;
640                        }
641                    }
642                }
643                Some(c) => {
644                    self.bump();
645                    value.push(c);
646                }
647            }
648        }
649        self.push(TokenKind::StringLit(value), pos, start);
650    }
651
652    /// `'a'`, `'\n'`, `'\x41'`. A lone `'` that doesn't close within a few
653    /// chars is not a char literal (identifiers consume their own primes, so
654    /// this only triggers at expression positions).
655    fn char_lit(&mut self, pos: Pos) {
656        let start = self.byte;
657        // Lookahead: find closing quote within a short window.
658        let mut j = self.i + 1;
659        let mut escaped = false;
660        let mut ok = false;
661        let window_end = (self.i + 12).min(self.chars.len());
662        while j < window_end {
663            match self.chars[j] {
664                '\\' if !escaped => escaped = true,
665                '\'' if !escaped => {
666                    ok = j > self.i + 1;
667                    break;
668                }
669                '\n' => break,
670                _ => escaped = false,
671            }
672            j += 1;
673        }
674        if !ok {
675            self.bump();
676            self.error(LexErrorKind::StraySingleQuote, pos);
677            return;
678        }
679        self.bump(); // opening '
680        let mut value = String::new();
681        while self.peek() != Some('\'') {
682            let c = self.bump().unwrap();
683            if c == '\\' {
684                let escape_pos = Pos {
685                    line: self.line,
686                    column: self.column.saturating_sub(1),
687                };
688                if let Some(e) = self.bump() {
689                    match unescape(e) {
690                        Some(c) => value.push(c),
691                        None => {
692                            self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
693                            value.push(e);
694                        }
695                    }
696                }
697            } else {
698                value.push(c);
699            }
700        }
701        self.bump(); // closing '
702        if value.chars().count() != 1 {
703            self.error(LexErrorKind::CharacterLiteralWrongLength, pos);
704        }
705        self.push(TokenKind::CharLit(value), pos, start);
706    }
707
708    fn number(&mut self, pos: Pos) {
709        let start = self.byte;
710        let mut text = String::new();
711        if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
712            text.push(self.bump().unwrap());
713            text.push(self.bump().unwrap());
714            let mut has_hex_digit = false;
715            while self
716                .peek()
717                .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
718            {
719                let c = self.bump().unwrap();
720                has_hex_digit |= c.is_ascii_hexdigit();
721                text.push(c);
722            }
723            if !has_hex_digit {
724                self.error(LexErrorKind::HexLiteralMissingDigits, pos);
725            }
726            self.push(TokenKind::IntLit(text), pos, start);
727            return;
728        }
729        while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
730            text.push(self.bump().unwrap());
731        }
732        let mut decimal = false;
733        // `1.5` is a decimal but `1..5` or `1.foo` is not.
734        if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
735            decimal = true;
736            text.push(self.bump().unwrap());
737            while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
738                text.push(self.bump().unwrap());
739            }
740        }
741        if matches!(self.peek(), Some('e' | 'E')) {
742            decimal = true;
743            if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
744                || (matches!(self.peek_at(1), Some('+' | '-'))
745                    && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
746            {
747                text.push(self.bump().unwrap());
748                if matches!(self.peek(), Some('+' | '-')) {
749                    text.push(self.bump().unwrap());
750                }
751                while self.peek().is_some_and(|c| c.is_ascii_digit()) {
752                    text.push(self.bump().unwrap());
753                }
754            } else {
755                text.push(self.bump().unwrap());
756                if matches!(self.peek(), Some('+' | '-')) {
757                    text.push(self.bump().unwrap());
758                }
759                self.error(LexErrorKind::DecimalExponentMissingDigits, pos);
760            }
761        }
762        if decimal {
763            self.push(TokenKind::DecimalLit(text), pos, start);
764        } else {
765            self.push(TokenKind::IntLit(text), pos, start);
766        }
767    }
768
769    /// Identifiers, with greedy qualification: `DA.Set.fromList` is one
770    /// token (qualifier "DA.Set", name "fromList").
771    fn identifier(&mut self, pos: Pos) {
772        let start = self.byte;
773        let mut segments: Vec<String> = Vec::new();
774        loop {
775            let mut seg = String::new();
776            while self.peek().is_some_and(is_ident_char) {
777                seg.push(self.bump().unwrap());
778            }
779            let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
780            segments.push(seg);
781            // Continue qualification only after an Upper segment: `Foo.bar`
782            // is qualified, `foo.bar` is composition/projection.
783            if seg_is_upper
784                && self.peek() == Some('.')
785                && self.peek_at(1).is_some_and(is_ident_start)
786            {
787                self.bump(); // .
788                continue;
789            }
790            break;
791        }
792        let name = segments.pop().unwrap();
793        let qualifier = if segments.is_empty() {
794            None
795        } else {
796            Some(segments.join("."))
797        };
798        let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
799            TokenKind::UpperId { qualifier, name }
800        } else {
801            TokenKind::LowerId { qualifier, name }
802        };
803        self.push(tok, pos, start);
804    }
805
806    fn operator(&mut self, pos: Pos) {
807        let start = self.i;
808        let byte_start = self.byte;
809        while self.peek().is_some_and(is_symbol_char) {
810            // `{-` inside an operator run can't happen ({ isn't a symbol
811            // char), but `--` comment detection needs the full run first.
812            self.bump();
813        }
814        let text: String = self.chars[start..self.i].iter().collect();
815        // A run of 2+ dashes and nothing else is a line comment (Haskell
816        // rule: `-->` is an operator, `--` and `---` start comments).
817        if text.len() >= 2 && text.chars().all(|c| c == '-') {
818            while self.peek().is_some_and(|c| c != '\n') {
819                self.bump();
820            }
821            self.push_trivia(TriviaKind::LineComment, pos, byte_start);
822            return;
823        }
824        self.push(TokenKind::Op(text), pos, byte_start);
825    }
826}
827
828const fn unescape(c: char) -> Option<char> {
829    match c {
830        'n' => Some('\n'),
831        't' => Some('\t'),
832        'r' => Some('\r'),
833        '0' => Some('\0'),
834        'a' => Some('\u{07}'),
835        'b' => Some('\u{08}'),
836        'f' => Some('\u{0c}'),
837        'v' => Some('\u{0b}'),
838        '"' => Some('"'),
839        '\'' => Some('\''),
840        '\\' => Some('\\'),
841        '&' => Some('&'),
842        // DAML follows Haskell-style text escapes, including numeric escapes
843        // (`\123`, `\o173`, `\x7B`) and named ASCII escapes (`\NUL`, `\SOH`,
844        // ...). This lexer preserves source spans rather than fully decoding
845        // multi-character escapes here, so accept the leading escape character
846        // and let the remaining source characters flow through unchanged.
847        '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
848        _ => None,
849    }
850}
851
852impl TokenKind {
853    /// The identifier text if this is an unqualified lowercase identifier —
854    /// how the parser checks for (contextual) keywords.
855    pub const fn keyword(&self) -> Option<&str> {
856        match self {
857            Self::LowerId {
858                qualifier: None,
859                name,
860            } => Some(name.as_str()),
861            _ => None,
862        }
863    }
864
865    pub fn is_keyword(&self, kw: &str) -> bool {
866        self.keyword() == Some(kw)
867    }
868
869    pub fn is_op(&self, op: &str) -> bool {
870        matches!(self, Self::Op(o) if o == op)
871    }
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877
878    fn toks(src: &str) -> Vec<TokenKind> {
879        let (tokens, errors) = lex(src).into_parts();
880        assert!(errors.is_empty(), "lex errors: {errors:?}");
881        tokens.into_iter().map(|t| t.kind).collect()
882    }
883
884    fn lex_error_messages(src: &str) -> Vec<String> {
885        let (_, errors) = lex(src).into_parts();
886        errors.into_iter().map(|e| e.to_string()).collect()
887    }
888
889    fn lower(name: &str) -> TokenKind {
890        TokenKind::LowerId {
891            qualifier: None,
892            name: name.to_string(),
893        }
894    }
895
896    fn upper(name: &str) -> TokenKind {
897        TokenKind::UpperId {
898            qualifier: None,
899            name: name.to_string(),
900        }
901    }
902
903    #[test]
904    fn line_comment_with_keywords_produces_no_tokens() {
905        assert_eq!(toks("-- electing to exercise the option"), vec![]);
906        assert_eq!(toks("--- template Foo"), vec![]);
907    }
908
909    #[test]
910    fn arrow_like_operator_is_not_comment() {
911        assert_eq!(
912            toks("a --> b"),
913            vec![lower("a"), TokenKind::Op("-->".into()), lower("b")]
914        );
915    }
916
917    #[test]
918    fn nested_block_comment() {
919        assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
920    }
921
922    #[test]
923    fn string_with_keyword_and_escapes() {
924        assert_eq!(
925            toks(r#""template \"Foo\" \n""#),
926            vec![TokenKind::StringLit("template \"Foo\" \n".into())]
927        );
928    }
929
930    #[test]
931    fn qualified_identifiers() {
932        assert_eq!(
933            toks("DA.Set.fromList Map.Map foo"),
934            vec![
935                TokenKind::LowerId {
936                    qualifier: Some("DA.Set".into()),
937                    name: "fromList".into()
938                },
939                TokenKind::UpperId {
940                    qualifier: Some("Map".into()),
941                    name: "Map".into()
942                },
943                lower("foo"),
944            ]
945        );
946    }
947
948    #[test]
949    fn numbers() {
950        assert_eq!(
951            toks("42 1.5 0x1F 2e3 1_000"),
952            vec![
953                TokenKind::IntLit("42".into()),
954                TokenKind::DecimalLit("1.5".into()),
955                TokenKind::IntLit("0x1F".into()),
956                TokenKind::DecimalLit("2e3".into()),
957                TokenKind::IntLit("1_000".into()),
958            ]
959        );
960    }
961
962    #[test]
963    fn malformed_hex_literal_reports_error() {
964        assert_eq!(
965            lex_error_messages("0x 0x_"),
966            vec![
967                "hex literal requires at least one digit",
968                "hex literal requires at least one digit",
969            ]
970        );
971    }
972
973    #[test]
974    fn malformed_decimal_exponent_reports_error() {
975        assert_eq!(
976            lex_error_messages("1e 1e+ 1e-"),
977            vec![
978                "decimal exponent requires at least one digit",
979                "decimal exponent requires at least one digit",
980                "decimal exponent requires at least one digit",
981            ]
982        );
983    }
984
985    #[test]
986    fn enum_from_to_is_not_decimal() {
987        assert_eq!(
988            toks("[1..5]"),
989            vec![
990                TokenKind::LBracket,
991                TokenKind::IntLit("1".into()),
992                TokenKind::Op("..".into()),
993                TokenKind::IntLit("5".into()),
994                TokenKind::RBracket,
995            ]
996        );
997    }
998
999    #[test]
1000    fn primes_stay_in_identifier_and_char_lit_works() {
1001        assert_eq!(
1002            toks(r"foo' 'a' '\n'"),
1003            vec![
1004                lower("foo'"),
1005                TokenKind::CharLit("a".into()),
1006                TokenKind::CharLit("\n".into())
1007            ]
1008        );
1009    }
1010
1011    #[test]
1012    fn invalid_escape_sequences_report_errors() {
1013        assert_eq!(
1014            lex_error_messages(r#""\q" '\q'"#),
1015            vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
1016        );
1017    }
1018
1019    #[test]
1020    fn multi_character_char_literal_reports_error() {
1021        let (tokens, errors) = lex("'ab'").into_parts();
1022        assert_eq!(
1023            tokens.iter().map(|t| t.kind.clone()).collect::<Vec<_>>(),
1024            vec![TokenKind::CharLit("ab".into())]
1025        );
1026        assert_eq!(
1027            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
1028            vec!["character literal must contain exactly one character".to_string()]
1029        );
1030    }
1031
1032    #[test]
1033    fn operators_and_punctuation() {
1034        assert_eq!(
1035            toks("x <- f (y, z) `div` 2"),
1036            vec![
1037                lower("x"),
1038                TokenKind::Op("<-".into()),
1039                lower("f"),
1040                TokenKind::LParen,
1041                lower("y"),
1042                TokenKind::Comma,
1043                lower("z"),
1044                TokenKind::RParen,
1045                TokenKind::Backtick,
1046                lower("div"),
1047                TokenKind::Backtick,
1048                TokenKind::IntLit("2".into()),
1049            ]
1050        );
1051    }
1052
1053    #[test]
1054    fn spans_are_one_based() {
1055        let (tokens, _) = lex("ab\n  cd").into_parts();
1056        assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
1057        assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
1058    }
1059
1060    #[test]
1061    fn tab_advances_to_stop() {
1062        let (tokens, _) = lex("\tx").into_parts();
1063        assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
1064    }
1065
1066    #[test]
1067    fn unterminated_string_is_error_not_hang() {
1068        let (_, errors) = lex("x = \"oops\ny").into_parts();
1069        assert_eq!(errors.len(), 1);
1070    }
1071
1072    #[test]
1073    fn unterminated_block_comment_is_error_not_hang() {
1074        let (_, errors) = lex("{- never closed").into_parts();
1075        assert_eq!(errors.len(), 1);
1076    }
1077
1078    fn trivia_of(src: &str) -> Vec<Trivia> {
1079        let (_, trivia, _) = lex_with_trivia(src).into_parts();
1080        trivia
1081    }
1082
1083    /// The lossless oracle on one source: spans must tile the file and the
1084    /// reconstruction must be byte-identical.
1085    fn assert_round_trip(src: &str) {
1086        let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1087        assert!(errors.is_empty(), "lex errors: {errors:?}");
1088        assert_eq!(
1089            render_lossless(src, &tokens, &trivia).as_deref(),
1090            Ok(src),
1091            "round trip failed for {src:?}"
1092        );
1093    }
1094
1095    #[test]
1096    fn line_comment_becomes_trivia_with_exact_text_and_span() {
1097        let src = "x = 1 -- electing to exercise\ny = 2\n";
1098        let trivia = trivia_of(src);
1099        assert_eq!(trivia.len(), 1);
1100        assert_eq!(trivia[0].kind, TriviaKind::LineComment);
1101        assert_eq!(trivia[0].text, "-- electing to exercise");
1102        assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
1103        assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
1104    }
1105
1106    #[test]
1107    fn nested_block_comment_becomes_one_trivia() {
1108        let src = "{- outer {- inner -} still -} x";
1109        let trivia = trivia_of(src);
1110        assert_eq!(trivia.len(), 1);
1111        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1112        assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
1113    }
1114
1115    #[test]
1116    fn unterminated_block_comment_still_yields_trivia_to_eof() {
1117        let (_, trivia, errors) = lex_with_trivia("x {- never closed").into_parts();
1118        assert_eq!(errors.len(), 1);
1119        assert_eq!(trivia.len(), 1);
1120        assert_eq!(trivia[0].text, "{- never closed");
1121    }
1122
1123    #[test]
1124    fn blank_lines_between_items_counted() {
1125        let src = "x = 1\n\n\ny = 2\n";
1126        let trivia = trivia_of(src);
1127        assert_eq!(trivia.len(), 1);
1128        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
1129        assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
1130    }
1131
1132    #[test]
1133    fn blank_line_at_file_start_counted() {
1134        let trivia = trivia_of("\nx = 1\n");
1135        assert_eq!(trivia.len(), 1);
1136        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
1137        assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
1138    }
1139
1140    #[test]
1141    fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
1142        let src = "x = 1 {- a\n\nb -}\ny = 2\n";
1143        let trivia = trivia_of(src);
1144        assert_eq!(trivia.len(), 1, "{trivia:?}");
1145        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1146    }
1147
1148    #[test]
1149    fn blank_line_between_comments_counted() {
1150        let src = "-- a\n\n-- b\nx = 1\n";
1151        let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
1152        assert_eq!(
1153            kinds,
1154            vec![
1155                TriviaKind::LineComment,
1156                TriviaKind::BlankLines(1),
1157                TriviaKind::LineComment,
1158            ]
1159        );
1160    }
1161
1162    #[test]
1163    fn cpp_directive_becomes_trivia() {
1164        let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
1165        let trivia = trivia_of(src);
1166        assert_eq!(trivia.len(), 2);
1167        assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1168        assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1169    }
1170
1171    #[test]
1172    fn round_trip_is_byte_identical() {
1173        assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1174        assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1175        assert_round_trip("\tärger = [1..5] -- ütf\n");
1176        assert_round_trip("s = \"gap \\  \\ here\"\n");
1177        assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1178        assert_round_trip("\n\n  \nf = 1  \n   ");
1179        assert_round_trip("");
1180    }
1181
1182    #[test]
1183    fn render_lossless_detects_lost_bytes() {
1184        let src = "x = 1 -- comment\n";
1185        let (tokens, mut trivia, _) = lex_with_trivia(src).into_parts();
1186        trivia.clear(); // simulate a lexer that drops the comment
1187        assert!(render_lossless(src, &tokens, &trivia).is_err());
1188    }
1189
1190    #[test]
1191    fn unicode_identifier() {
1192        assert_eq!(
1193            toks("ärger = 1"),
1194            vec![
1195                lower("ärger"),
1196                TokenKind::Op("=".into()),
1197                TokenKind::IntLit("1".into())
1198            ]
1199        );
1200        let _ = upper("Ülf"); // helper used
1201    }
1202}