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