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/// A small domain type for identifier-like text.
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
10pub struct Identifier(String);
11
12impl Identifier {
13    #[must_use]
14    pub const fn as_str(&self) -> &str {
15        self.0.as_str()
16    }
17}
18
19impl AsRef<str> for Identifier {
20    fn as_ref(&self) -> &str {
21        self.as_str()
22    }
23}
24
25impl std::fmt::Display for Identifier {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(self.as_str())
28    }
29}
30
31impl From<String> for Identifier {
32    fn from(value: String) -> Self {
33        Self(value)
34    }
35}
36
37impl From<&str> for Identifier {
38    fn from(value: &str) -> Self {
39        Self(value.to_string())
40    }
41}
42
43impl From<Identifier> for String {
44    fn from(value: Identifier) -> Self {
45        value.0
46    }
47}
48
49impl std::ops::Deref for Identifier {
50    type Target = str;
51    fn deref(&self) -> &Self::Target {
52        self.as_str()
53    }
54}
55
56impl std::borrow::Borrow<str> for Identifier {
57    fn borrow(&self) -> &str {
58        self.as_str()
59    }
60}
61
62impl PartialEq<&str> for Identifier {
63    fn eq(&self, other: &&str) -> bool {
64        self.as_str() == *other
65    }
66}
67
68impl PartialEq<Identifier> for &str {
69    fn eq(&self, other: &Identifier) -> bool {
70        *self == other.as_str()
71    }
72}
73
74impl PartialEq<String> for Identifier {
75    fn eq(&self, other: &String) -> bool {
76        self.as_str() == other.as_str()
77    }
78}
79
80impl PartialEq<Identifier> for String {
81    fn eq(&self, other: &Identifier) -> bool {
82        self.as_str() == other.as_str()
83    }
84}
85
86/// A small domain type for symbolic operator text.
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
88pub struct Operator(String);
89
90impl Operator {
91    #[must_use]
92    pub const fn as_str(&self) -> &str {
93        self.0.as_str()
94    }
95}
96
97impl AsRef<str> for Operator {
98    fn as_ref(&self) -> &str {
99        self.as_str()
100    }
101}
102
103impl std::fmt::Display for Operator {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.write_str(self.as_str())
106    }
107}
108
109impl From<String> for Operator {
110    fn from(value: String) -> Self {
111        Self(value)
112    }
113}
114
115impl From<&str> for Operator {
116    fn from(value: &str) -> Self {
117        Self(value.to_string())
118    }
119}
120
121impl From<Operator> for String {
122    fn from(value: Operator) -> Self {
123        value.0
124    }
125}
126
127impl std::ops::Deref for Operator {
128    type Target = str;
129    fn deref(&self) -> &Self::Target {
130        self.as_str()
131    }
132}
133
134impl std::borrow::Borrow<str> for Operator {
135    fn borrow(&self) -> &str {
136        self.as_str()
137    }
138}
139
140impl PartialEq<&str> for Operator {
141    fn eq(&self, other: &&str) -> bool {
142        self.as_str() == *other
143    }
144}
145
146impl PartialEq<Operator> for &str {
147    fn eq(&self, other: &Operator) -> bool {
148        *self == other.as_str()
149    }
150}
151
152impl PartialEq<String> for Operator {
153    fn eq(&self, other: &String) -> bool {
154        self.as_str() == other.as_str()
155    }
156}
157
158impl PartialEq<Operator> for String {
159    fn eq(&self, other: &Operator) -> bool {
160        self.as_str() == other.as_str()
161    }
162}
163
164/// A small domain type for module-style qualified names (`DA.Map`, `Daml.Foo`).
165#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
166pub struct ModuleName(String);
167
168impl ModuleName {
169    #[must_use]
170    pub const fn as_str(&self) -> &str {
171        self.0.as_str()
172    }
173}
174
175impl AsRef<str> for ModuleName {
176    fn as_ref(&self) -> &str {
177        self.as_str()
178    }
179}
180
181impl std::fmt::Display for ModuleName {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        f.write_str(self.as_str())
184    }
185}
186
187impl From<String> for ModuleName {
188    fn from(value: String) -> Self {
189        Self(value)
190    }
191}
192
193impl From<&str> for ModuleName {
194    fn from(value: &str) -> Self {
195        Self(value.to_string())
196    }
197}
198
199impl From<Identifier> for ModuleName {
200    fn from(value: Identifier) -> Self {
201        Self(value.0)
202    }
203}
204
205impl From<ModuleName> for String {
206    fn from(value: ModuleName) -> Self {
207        value.0
208    }
209}
210
211impl std::ops::Deref for ModuleName {
212    type Target = str;
213    fn deref(&self) -> &Self::Target {
214        self.as_str()
215    }
216}
217
218impl std::borrow::Borrow<str> for ModuleName {
219    fn borrow(&self) -> &str {
220        self.as_str()
221    }
222}
223
224impl PartialEq<&str> for ModuleName {
225    fn eq(&self, other: &&str) -> bool {
226        self.as_str() == *other
227    }
228}
229
230impl PartialEq<ModuleName> for &str {
231    fn eq(&self, other: &ModuleName) -> bool {
232        *self == other.as_str()
233    }
234}
235
236impl PartialEq<String> for ModuleName {
237    fn eq(&self, other: &String) -> bool {
238        self.as_str() == other.as_str()
239    }
240}
241
242impl PartialEq<ModuleName> for String {
243    fn eq(&self, other: &ModuleName) -> bool {
244        self.as_str() == other.as_str()
245    }
246}
247
248/// 1-based source position of a token's first character.
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct Pos {
251    pub line: usize,
252    pub column: usize,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum TokenKind {
258    /// Lowercase-initial identifier, possibly qualified: `foo`, `Map.lookup`.
259    LowerId {
260        qualifier: Option<ModuleName>,
261        name: Identifier,
262    },
263    /// Uppercase-initial identifier, possibly qualified: `Foo`, `DA.Set.Set`.
264    UpperId {
265        qualifier: Option<ModuleName>,
266        name: Identifier,
267    },
268    /// Symbolic operator: `+`, `<-`, `->`, `=`, `=>`, `::`, `.`, `\`, ...
269    Op(Operator),
270    IntLit(String),
271    DecimalLit(String),
272    StringLit(String),
273    CharLit(String),
274    LParen,
275    RParen,
276    LBracket,
277    RBracket,
278    LBrace,
279    RBrace,
280    Comma,
281    Semi,
282    Backtick,
283    /// Layout-inserted virtual open brace (block start).
284    VLBrace,
285    /// Layout-inserted virtual close brace (block end).
286    VRBrace,
287    /// Layout-inserted virtual semicolon (new item at block indentation).
288    VSemi,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct Token {
293    pub(crate) kind: TokenKind,
294    pub(crate) pos: Pos,
295    /// Byte offset of the token's first character in the source.
296    /// Virtual layout tokens are zero-width (`start == end`).
297    pub(crate) start: usize,
298    /// Byte offset one past the token's last character.
299    pub(crate) end: usize,
300}
301
302impl Token {
303    #[must_use]
304    pub const fn kind(&self) -> &TokenKind {
305        &self.kind
306    }
307
308    #[must_use]
309    pub const fn pos(&self) -> Pos {
310        self.pos
311    }
312
313    #[must_use]
314    pub const fn start(&self) -> usize {
315        self.start
316    }
317
318    #[must_use]
319    pub const fn end(&self) -> usize {
320        self.end
321    }
322
323    /// Layout-inserted tokens carry no source bytes (they are zero-width);
324    /// AST node-span computation skips them so spans tile the real source.
325    #[must_use]
326    pub const fn is_virtual(&self) -> bool {
327        matches!(
328            self.kind,
329            TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
330        )
331    }
332}
333
334/// Source text the lexer consumes but the parser never sees.
335///
336/// Carries exact byte spans so a printer can re-attach comments to nearby AST
337/// nodes (which already have positions) and reproduce the original bytes.
338#[derive(Debug, Clone, PartialEq, Eq)]
339#[non_exhaustive]
340pub enum TriviaKind {
341    /// `-- ...` to end of line (newline not included).
342    LineComment,
343    /// `{- ... -}`, possibly nested; unterminated runs to EOF.
344    BlockComment,
345    /// `#ifdef`/`#endif`/... preprocessor line at column 1.
346    CppDirective,
347    /// A run of N whitespace-only lines between tokens/comments.
348    BlankLines(usize),
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct Trivia {
353    pub(crate) kind: TriviaKind,
354    /// Exact source slice (delimiters included; empty for `BlankLines`).
355    pub(crate) text: String,
356    pub(crate) pos: Pos,
357    pub(crate) start: usize,
358    pub(crate) end: usize,
359}
360
361impl Trivia {
362    #[must_use]
363    pub const fn kind(&self) -> &TriviaKind {
364        &self.kind
365    }
366
367    #[must_use]
368    pub fn text(&self) -> &str {
369        &self.text
370    }
371
372    #[must_use]
373    pub const fn pos(&self) -> Pos {
374        self.pos
375    }
376
377    #[must_use]
378    pub const fn start(&self) -> usize {
379        self.start
380    }
381
382    #[must_use]
383    pub const fn end(&self) -> usize {
384        self.end
385    }
386}
387
388/// A lexical error. The scan must survive these: the caller reports the
389/// diagnostic and works with the tokens produced so far.
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct LexError {
392    pub kind: LexErrorKind,
393    pub pos: Pos,
394}
395
396impl std::fmt::Display for LexError {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        self.kind.fmt(f)
399    }
400}
401
402impl LexError {
403    /// Byte span of the offending range, where available.
404    #[must_use]
405    pub fn byte_range_in(&self, source: &str) -> std::ops::Range<usize> {
406        let start = byte_of_pos(source, self.pos);
407        if start >= source.len() {
408            return start..start;
409        }
410
411        match &self.kind {
412            LexErrorKind::UnexpectedCharacter(c) => {
413                let end = (start + c.len_utf8()).min(source.len());
414                start..end
415            }
416            LexErrorKind::UnterminatedStringLiteral
417            | LexErrorKind::UnterminatedStringGap
418            | LexErrorKind::StraySingleQuote => {
419                let end = (start + 1).min(source.len());
420                start..end
421            }
422            LexErrorKind::UnterminatedBlockComment => start..source.len(),
423            LexErrorKind::InvalidEscapeSequence(_) => {
424                let first = char_len_at(source, start).unwrap_or(0);
425                let mut end = (start + first).min(source.len());
426                if let Some(second) = source.get(end..).and_then(|s| s.chars().next()) {
427                    end = (end + second.len_utf8()).min(source.len());
428                }
429                start..end
430            }
431            LexErrorKind::CharacterLiteralWrongLength => start..end_char_lit_error(source, start),
432            LexErrorKind::HexLiteralMissingDigits => start..end_hex_missing_digits(source, start),
433            LexErrorKind::DecimalExponentMissingDigits => {
434                start..end_decimal_exponent_missing_digits(source, start)
435            }
436        }
437    }
438}
439impl std::error::Error for LexError {}
440
441#[inline]
442fn char_len_at(source: &str, byte: usize) -> Option<usize> {
443    source
444        .get(byte..)
445        .and_then(|s| s.chars().next())
446        .map(char::len_utf8)
447}
448
449fn end_char_lit_error(source: &str, start: usize) -> usize {
450    if start >= source.len() {
451        return start;
452    }
453    if char_len_at(source, start).is_none() {
454        return start;
455    }
456
457    let mut i = start + char_len_at(source, start).unwrap_or(0);
458    let mut escaped = false;
459    while i < source.len() {
460        let len = char_len_at(source, i).unwrap_or(0);
461        if len == 0 {
462            return start + 1;
463        }
464        let ch = source[i..i + len].chars().next().unwrap();
465        i += len;
466
467        if escaped {
468            escaped = false;
469            continue;
470        }
471        if ch == '\\' {
472            escaped = true;
473            continue;
474        }
475        if ch == '\'' {
476            return i;
477        }
478    }
479
480    start + 1
481}
482
483fn end_hex_missing_digits(source: &str, start: usize) -> usize {
484    if start >= source.len() {
485        return start;
486    }
487    let after_prefix = start.saturating_add(2);
488    let prefix = source.get(start..after_prefix).unwrap_or("");
489    if prefix != "0x" && prefix != "0X" {
490        return start;
491    }
492
493    let mut end = after_prefix;
494    while let Some(len) = char_len_at(source, end) {
495        if source.get(end..end + len) != Some("_") {
496            break;
497        }
498        end += len;
499    }
500    end
501}
502
503fn end_decimal_exponent_missing_digits(source: &str, start: usize) -> usize {
504    let mut byte = start;
505    while byte < source.len() {
506        let len = match char_len_at(source, byte) {
507            Some(len) => len,
508            None => return start,
509        };
510        if len == 0 {
511            return start;
512        }
513        let ch = source[byte..byte + len].chars().next().unwrap();
514        if matches!(ch, 'e' | 'E') {
515            let mut end = byte + len;
516            if let Some(sign_len) = char_len_at(source, end) {
517                let sign = source[end..end + sign_len].chars().next().unwrap();
518                if matches!(sign, '+' | '-') {
519                    end += sign_len;
520                }
521            }
522            return end;
523        }
524
525        if ch.is_whitespace() {
526            return start;
527        }
528        byte += len;
529    }
530
531    start
532}
533
534/// Byte offset of a 1-based (line, column) position.
535#[inline]
536fn byte_of_pos(source: &str, pos: Pos) -> usize {
537    let mut line = 1usize;
538    let mut col = 1usize;
539    for (idx, ch) in source.char_indices() {
540        if line == pos.line && col == pos.column {
541            return idx;
542        }
543        match ch {
544            '\n' => {
545                line += 1;
546                col = 1;
547            }
548            '\t' => col = ((col - 1) / TAB_STOP + 1) * TAB_STOP + 1,
549            _ => col += 1,
550        }
551    }
552    source.len()
553}
554
555#[derive(Debug, Clone, PartialEq, Eq)]
556#[non_exhaustive]
557pub enum LexErrorKind {
558    UnexpectedCharacter(char),
559    UnterminatedBlockComment,
560    UnterminatedStringLiteral,
561    UnterminatedStringGap,
562    InvalidEscapeSequence(char),
563    StraySingleQuote,
564    CharacterLiteralWrongLength,
565    HexLiteralMissingDigits,
566    DecimalExponentMissingDigits,
567}
568
569impl std::fmt::Display for LexErrorKind {
570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571        match self {
572            Self::UnexpectedCharacter(c) => write!(f, "unexpected character '{c}'"),
573            Self::UnterminatedBlockComment => f.write_str("unterminated block comment"),
574            Self::UnterminatedStringLiteral => f.write_str("unterminated string literal"),
575            Self::UnterminatedStringGap => f.write_str("unterminated string gap"),
576            Self::InvalidEscapeSequence(c) => write!(f, "invalid escape sequence \\{c}"),
577            Self::StraySingleQuote => f.write_str("stray single quote"),
578            Self::CharacterLiteralWrongLength => {
579                f.write_str("character literal must contain exactly one character")
580            }
581            Self::HexLiteralMissingDigits => f.write_str("hex literal requires at least one digit"),
582            Self::DecimalExponentMissingDigits => {
583                f.write_str("decimal exponent requires at least one digit")
584            }
585        }
586    }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub struct LexOutput {
591    pub tokens: Vec<Token>,
592    pub errors: Vec<LexError>,
593}
594
595impl LexOutput {
596    #[must_use]
597    pub fn into_parts(self) -> (Vec<Token>, Vec<LexError>) {
598        (self.tokens, self.errors)
599    }
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct LexWithTriviaOutput {
604    pub tokens: Vec<Token>,
605    pub trivia: Vec<Trivia>,
606    pub errors: Vec<LexError>,
607}
608
609impl LexWithTriviaOutput {
610    #[must_use]
611    pub fn into_parts(self) -> (Vec<Token>, Vec<Trivia>, Vec<LexError>) {
612        (self.tokens, self.trivia, self.errors)
613    }
614}
615
616#[derive(Debug, Clone, PartialEq, Eq)]
617#[non_exhaustive]
618pub enum RenderLosslessError {
619    OverlappingSpans {
620        start: usize,
621    },
622    UncoveredBytes {
623        start: usize,
624        end: usize,
625        text: String,
626    },
627    UncoveredTail {
628        start: usize,
629        text: String,
630    },
631}
632
633impl std::fmt::Display for RenderLosslessError {
634    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
635        match self {
636            Self::OverlappingSpans { start } => write!(f, "overlapping spans at byte {start}"),
637            Self::UncoveredBytes { start, end, text } => write!(
638                f,
639                "bytes {start}..{end} lost (not covered by any token/trivia): {text:?}"
640            ),
641            Self::UncoveredTail { start, text } => {
642                write!(f, "bytes {start}.. lost at EOF: {text:?}")
643            }
644        }
645    }
646}
647
648impl std::error::Error for RenderLosslessError {}
649
650const fn is_symbol_char(c: char) -> bool {
651    matches!(
652        c,
653        '!' | '#'
654            | '$'
655            | '%'
656            | '&'
657            | '*'
658            | '+'
659            | '.'
660            | '/'
661            | '<'
662            | '='
663            | '>'
664            | '?'
665            | '@'
666            | '\\'
667            | '^'
668            | '|'
669            | '-'
670            | '~'
671            | ':'
672    )
673}
674
675fn is_ident_start(c: char) -> bool {
676    c.is_alphabetic() || c == '_'
677}
678
679fn is_ident_char(c: char) -> bool {
680    c.is_alphanumeric() || c == '_' || c == '\''
681}
682
683pub(crate) const TAB_STOP: usize = 8;
684
685struct Lexer<'a> {
686    chars: Vec<char>,
687    src: &'a str,
688    i: usize,
689    byte: usize,
690    line: usize,
691    column: usize,
692    tokens: Vec<Token>,
693    trivia: Vec<Trivia>,
694    errors: Vec<LexError>,
695}
696
697/// Lex `source` into tokens and lexical errors only.
698///
699/// Use [`lex_with_trivia`] when callers also need comments and trivia.
700#[must_use]
701pub fn lex(source: &str) -> LexOutput {
702    let LexWithTriviaOutput { tokens, errors, .. } = lex_with_trivia(source);
703    LexOutput { tokens, errors }
704}
705
706/// Lex `source` into tokens, trivia, and lexical errors.
707#[must_use]
708pub fn lex_with_trivia(source: &str) -> LexWithTriviaOutput {
709    let mut lexer = Lexer {
710        chars: source.chars().collect(),
711        src: source,
712        i: 0,
713        byte: 0,
714        line: 1,
715        column: 1,
716        tokens: Vec::new(),
717        trivia: Vec::new(),
718        errors: Vec::new(),
719    };
720    lexer.scan_tokens();
721    let mut trivia = lexer.trivia;
722    add_blank_line_trivia(source, &lexer.tokens, &mut trivia);
723    trivia.sort_by_key(|t| t.start);
724    LexWithTriviaOutput {
725        tokens: lexer.tokens,
726        trivia,
727        errors: lexer.errors,
728    }
729}
730
731/// Reconstruct the source from token and trivia spans.
732///
733/// `Ok` only when the spans tile the file — every non-whitespace byte inside
734/// exactly one token or comment span — in which case the result is
735/// byte-identical to `source`. This is the lossless-trivia oracle for the
736/// formatter.
737#[must_use = "handle render errors instead of discarding"]
738pub fn render_lossless(
739    source: &str,
740    tokens: &[Token],
741    trivia: &[Trivia],
742) -> Result<String, RenderLosslessError> {
743    let mut items: Vec<(usize, usize)> = tokens
744        .iter()
745        .filter(|t| {
746            !matches!(
747                t.kind,
748                TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
749            )
750        })
751        .map(|t| (t.start, t.end))
752        .chain(
753            trivia
754                .iter()
755                .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
756                .map(|t| (t.start, t.end)),
757        )
758        .collect();
759    items.sort_unstable();
760    let mut out = String::with_capacity(source.len());
761    let mut prev = 0usize;
762    for (start, end) in items {
763        if start < prev {
764            return Err(RenderLosslessError::OverlappingSpans { start });
765        }
766        let gap = &source[prev..start];
767        if !gap.chars().all(char::is_whitespace) {
768            return Err(RenderLosslessError::UncoveredBytes {
769                start: prev,
770                end: start,
771                text: gap.to_string(),
772            });
773        }
774        out.push_str(gap);
775        out.push_str(&source[start..end]);
776        prev = end;
777    }
778    let tail = &source[prev..];
779    if !tail.chars().all(char::is_whitespace) {
780        return Err(RenderLosslessError::UncoveredTail {
781            start: prev,
782            text: tail.to_string(),
783        });
784    }
785    out.push_str(tail);
786    Ok(out)
787}
788
789/// A blank line is a whitespace-only line lying entirely between spans (so a
790/// blank-looking line inside a block comment or multiline string does not
791/// count). Emitted as one `BlankLines(n)` per maximal run so the printer can
792/// preserve paragraph breaks.
793fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
794    let mut spans: Vec<(usize, usize)> = tokens
795        .iter()
796        .map(|t| (t.start, t.end))
797        .chain(trivia.iter().map(|t| (t.start, t.end)))
798        .collect();
799    spans.sort_unstable();
800    let bytes = source.as_bytes();
801    let mut blanks = Vec::new();
802    let mut gap_start = 0usize;
803    let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
804        // Newline offsets inside the gap. Full lines between two newlines
805        // (or before the first newline when the gap starts the file) are
806        // blank by construction: the gap holds no token/comment bytes, and
807        // any stray non-whitespace there fails the lossless render anyway.
808        let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
809        // Interior gap: the partial line before the first newline belongs to
810        // the preceding token's line, so only lines between newlines count.
811        // A gap at byte 0 has no such partial line — line 1 itself is blank.
812        let count = if from == 0 {
813            newlines.len()
814        } else {
815            newlines.len().saturating_sub(1)
816        };
817        if count == 0 {
818            return;
819        }
820        let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
821        let region_end = newlines[newlines.len() - 1] + 1;
822        let line = source[..region_start].matches('\n').count() + 1;
823        out.push(Trivia {
824            kind: TriviaKind::BlankLines(count),
825            text: String::new(),
826            pos: Pos { line, column: 1 },
827            start: region_start,
828            end: region_end,
829        });
830    };
831    for &(s, e) in &spans {
832        if s > gap_start {
833            emit_gap(gap_start, s, &mut blanks);
834        }
835        gap_start = gap_start.max(e);
836    }
837    if source.len() > gap_start {
838        emit_gap(gap_start, source.len(), &mut blanks);
839    }
840    trivia.extend(blanks);
841}
842
843impl<'a> Lexer<'a> {
844    fn peek(&self) -> Option<char> {
845        self.chars.get(self.i).copied()
846    }
847
848    fn peek_at(&self, n: usize) -> Option<char> {
849        self.chars.get(self.i + n).copied()
850    }
851
852    fn bump(&mut self) -> Option<char> {
853        let c = self.chars.get(self.i).copied()?;
854        self.i += 1;
855        self.byte += c.len_utf8();
856        match c {
857            '\n' => {
858                self.line += 1;
859                self.column = 1;
860            }
861            '\t' => {
862                // Tab advances to the next multiple-of-8 stop, matching GHC,
863                // so mixed tabs/spaces don't silently corrupt layout.
864                self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
865            }
866            _ => self.column += 1,
867        }
868        Some(c)
869    }
870
871    const fn pos(&self) -> Pos {
872        Pos {
873            line: self.line,
874            column: self.column,
875        }
876    }
877
878    /// `start` is the token's first byte; its end is wherever the cursor is
879    /// now, so call this immediately after consuming the token.
880    fn push(&mut self, tok: TokenKind, pos: Pos, start: usize) {
881        self.tokens.push(Token {
882            kind: tok,
883            pos,
884            start,
885            end: self.byte,
886        });
887    }
888
889    fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
890        self.trivia.push(Trivia {
891            kind,
892            text: self.src[start..self.byte].to_string(),
893            pos,
894            start,
895            end: self.byte,
896        });
897    }
898
899    fn error(&mut self, kind: LexErrorKind, pos: Pos) {
900        self.errors.push(LexError { kind, pos });
901    }
902
903    fn scan_tokens(&mut self) {
904        while let Some(c) = self.peek() {
905            let pos = self.pos();
906            let start = self.byte;
907            match c {
908                ' ' | '\t' | '\n' | '\r' => {
909                    self.bump();
910                }
911                '(' => {
912                    self.bump();
913                    self.push(TokenKind::LParen, pos, start);
914                }
915                ')' => {
916                    self.bump();
917                    self.push(TokenKind::RParen, pos, start);
918                }
919                '[' => {
920                    self.bump();
921                    self.push(TokenKind::LBracket, pos, start);
922                }
923                ']' => {
924                    self.bump();
925                    self.push(TokenKind::RBracket, pos, start);
926                }
927                ',' => {
928                    self.bump();
929                    self.push(TokenKind::Comma, pos, start);
930                }
931                ';' => {
932                    self.bump();
933                    self.push(TokenKind::Semi, pos, start);
934                }
935                '`' => {
936                    self.bump();
937                    self.push(TokenKind::Backtick, pos, start);
938                }
939                '{' => {
940                    if self.peek_at(1) == Some('-') {
941                        self.block_comment(pos);
942                    } else {
943                        self.bump();
944                        self.push(TokenKind::LBrace, pos, start);
945                    }
946                }
947                '}' => {
948                    self.bump();
949                    self.push(TokenKind::RBrace, pos, start);
950                }
951                // CPP preprocessor directive (#ifdef/#endif/#include...) at
952                // column 1 — daml-prim/stdlib sources use {-# LANGUAGE CPP #-};
953                // directives are line-based, skip the whole line.
954                '#' if self.column == 1
955                    && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
956                {
957                    while self.peek().is_some_and(|c| c != '\n') {
958                        self.bump();
959                    }
960                    self.push_trivia(TriviaKind::CppDirective, pos, start);
961                }
962                '"' => self.string_lit(pos),
963                '\'' => self.char_lit(pos),
964                c if c.is_ascii_digit() => self.number(pos),
965                c if is_ident_start(c) => self.identifier(pos),
966                c if is_symbol_char(c) => self.operator(pos),
967                _ => {
968                    self.bump();
969                    self.error(LexErrorKind::UnexpectedCharacter(c), pos);
970                }
971            }
972        }
973    }
974
975    /// `{- ... -}`, nested as in Haskell. Unterminated comment is an error
976    /// but consumes to EOF (no hang, no panic).
977    fn block_comment(&mut self, pos: Pos) {
978        let start = self.byte;
979        self.bump(); // {
980        self.bump(); // -
981        let mut depth = 1usize;
982        while depth > 0 {
983            match self.peek() {
984                None => {
985                    self.error(LexErrorKind::UnterminatedBlockComment, pos);
986                    self.push_trivia(TriviaKind::BlockComment, pos, start);
987                    return;
988                }
989                Some('{') if self.peek_at(1) == Some('-') => {
990                    self.bump();
991                    self.bump();
992                    depth += 1;
993                }
994                Some('-') if self.peek_at(1) == Some('}') => {
995                    self.bump();
996                    self.bump();
997                    depth -= 1;
998                }
999                Some(_) => {
1000                    self.bump();
1001                }
1002            }
1003        }
1004        self.push_trivia(TriviaKind::BlockComment, pos, start);
1005    }
1006
1007    fn string_lit(&mut self, pos: Pos) {
1008        let start = self.byte;
1009        self.bump(); // opening "
1010        let mut value = String::new();
1011        loop {
1012            match self.peek() {
1013                None | Some('\n') => {
1014                    self.error(LexErrorKind::UnterminatedStringLiteral, pos);
1015                    break;
1016                }
1017                Some('"') => {
1018                    self.bump();
1019                    break;
1020                }
1021                Some('\\') => {
1022                    let escape_pos = self.pos();
1023                    self.bump();
1024                    match self.peek() {
1025                        // String gap: backslash, whitespace, backslash.
1026                        Some(w) if w.is_whitespace() => {
1027                            while self.peek().is_some_and(|c| c.is_whitespace()) {
1028                                self.bump();
1029                            }
1030                            if self.peek() == Some('\\') {
1031                                self.bump();
1032                            } else {
1033                                self.error(LexErrorKind::UnterminatedStringGap, pos);
1034                                break;
1035                            }
1036                        }
1037                        Some(e) => {
1038                            self.bump();
1039                            match unescape(e) {
1040                                Some(c) => value.push(c),
1041                                None => {
1042                                    self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
1043                                    value.push(e);
1044                                }
1045                            }
1046                        }
1047                        None => {
1048                            self.error(LexErrorKind::UnterminatedStringLiteral, pos);
1049                            break;
1050                        }
1051                    }
1052                }
1053                Some(c) => {
1054                    self.bump();
1055                    value.push(c);
1056                }
1057            }
1058        }
1059        self.push(TokenKind::StringLit(value), pos, start);
1060    }
1061
1062    /// `'a'`, `'\n'`, `'\x41'`. A lone `'` that doesn't close within a few
1063    /// chars is not a char literal (identifiers consume their own primes, so
1064    /// this only triggers at expression positions).
1065    fn char_lit(&mut self, pos: Pos) {
1066        let start = self.byte;
1067        // Lookahead: find closing quote within a short window.
1068        let mut j = self.i + 1;
1069        let mut escaped = false;
1070        let mut ok = false;
1071        let window_end = (self.i + 12).min(self.chars.len());
1072        while j < window_end {
1073            match self.chars[j] {
1074                '\\' if !escaped => escaped = true,
1075                '\'' if !escaped => {
1076                    ok = j > self.i + 1;
1077                    break;
1078                }
1079                '\n' => break,
1080                _ => escaped = false,
1081            }
1082            j += 1;
1083        }
1084        if !ok {
1085            self.bump();
1086            self.error(LexErrorKind::StraySingleQuote, pos);
1087            return;
1088        }
1089        self.bump(); // opening '
1090        let mut value = String::new();
1091        while self.peek() != Some('\'') {
1092            let c = self.bump().unwrap();
1093            if c == '\\' {
1094                let escape_pos = Pos {
1095                    line: self.line,
1096                    column: self.column.saturating_sub(1),
1097                };
1098                if let Some(e) = self.bump() {
1099                    match unescape(e) {
1100                        Some(c) => value.push(c),
1101                        None => {
1102                            self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
1103                            value.push(e);
1104                        }
1105                    }
1106                }
1107            } else {
1108                value.push(c);
1109            }
1110        }
1111        self.bump(); // closing '
1112        if value.chars().count() != 1 {
1113            self.error(LexErrorKind::CharacterLiteralWrongLength, pos);
1114        }
1115        self.push(TokenKind::CharLit(value), pos, start);
1116    }
1117
1118    fn number(&mut self, pos: Pos) {
1119        let start = self.byte;
1120        let mut text = String::new();
1121        if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
1122            text.push(self.bump().unwrap());
1123            text.push(self.bump().unwrap());
1124            let mut has_hex_digit = false;
1125            while self
1126                .peek()
1127                .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
1128            {
1129                let c = self.bump().unwrap();
1130                has_hex_digit |= c.is_ascii_hexdigit();
1131                text.push(c);
1132            }
1133            if !has_hex_digit {
1134                self.error(LexErrorKind::HexLiteralMissingDigits, pos);
1135            }
1136            self.push(TokenKind::IntLit(text), pos, start);
1137            return;
1138        }
1139        while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
1140            text.push(self.bump().unwrap());
1141        }
1142        let mut decimal = false;
1143        // `1.5` is a decimal but `1..5` or `1.foo` is not.
1144        if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
1145            decimal = true;
1146            text.push(self.bump().unwrap());
1147            while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
1148                text.push(self.bump().unwrap());
1149            }
1150        }
1151        if matches!(self.peek(), Some('e' | 'E')) {
1152            decimal = true;
1153            if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
1154                || (matches!(self.peek_at(1), Some('+' | '-'))
1155                    && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
1156            {
1157                text.push(self.bump().unwrap());
1158                if matches!(self.peek(), Some('+' | '-')) {
1159                    text.push(self.bump().unwrap());
1160                }
1161                while self.peek().is_some_and(|c| c.is_ascii_digit()) {
1162                    text.push(self.bump().unwrap());
1163                }
1164            } else {
1165                text.push(self.bump().unwrap());
1166                if matches!(self.peek(), Some('+' | '-')) {
1167                    text.push(self.bump().unwrap());
1168                }
1169                self.error(LexErrorKind::DecimalExponentMissingDigits, pos);
1170            }
1171        }
1172        if decimal {
1173            self.push(TokenKind::DecimalLit(text), pos, start);
1174        } else {
1175            self.push(TokenKind::IntLit(text), pos, start);
1176        }
1177    }
1178
1179    /// Identifiers, with greedy qualification: `DA.Set.fromList` is one
1180    /// token (qualifier "DA.Set", name "fromList").
1181    fn identifier(&mut self, pos: Pos) {
1182        let start = self.byte;
1183        let mut segments: Vec<String> = Vec::new();
1184        loop {
1185            let mut seg = String::new();
1186            while self.peek().is_some_and(is_ident_char) {
1187                seg.push(self.bump().unwrap());
1188            }
1189            let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
1190            segments.push(seg);
1191            // Continue qualification only after an Upper segment: `Foo.bar`
1192            // is qualified, `foo.bar` is composition/projection.
1193            if seg_is_upper
1194                && self.peek() == Some('.')
1195                && self.peek_at(1).is_some_and(is_ident_start)
1196            {
1197                self.bump(); // .
1198                continue;
1199            }
1200            break;
1201        }
1202        let name = segments.pop().unwrap();
1203        let qualifier = if segments.is_empty() {
1204            None
1205        } else {
1206            Some(segments.join(".").into())
1207        };
1208        let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
1209            TokenKind::UpperId {
1210                qualifier,
1211                name: name.into(),
1212            }
1213        } else {
1214            TokenKind::LowerId {
1215                qualifier,
1216                name: name.into(),
1217            }
1218        };
1219        self.push(tok, pos, start);
1220    }
1221
1222    fn operator(&mut self, pos: Pos) {
1223        let start = self.i;
1224        let byte_start = self.byte;
1225        while self.peek().is_some_and(is_symbol_char) {
1226            // `{-` inside an operator run can't happen ({ isn't a symbol
1227            // char), but `--` comment detection needs the full run first.
1228            self.bump();
1229        }
1230        let text: String = self.chars[start..self.i].iter().collect();
1231        // A run of 2+ dashes and nothing else is a line comment (Haskell
1232        // rule: `-->` is an operator, `--` and `---` start comments).
1233        if text.len() >= 2 && text.chars().all(|c| c == '-') {
1234            while self.peek().is_some_and(|c| c != '\n') {
1235                self.bump();
1236            }
1237            self.push_trivia(TriviaKind::LineComment, pos, byte_start);
1238            return;
1239        }
1240        self.push(TokenKind::Op(text.into()), pos, byte_start);
1241    }
1242}
1243
1244const fn unescape(c: char) -> Option<char> {
1245    match c {
1246        'n' => Some('\n'),
1247        't' => Some('\t'),
1248        'r' => Some('\r'),
1249        '0' => Some('\0'),
1250        'a' => Some('\u{07}'),
1251        'b' => Some('\u{08}'),
1252        'f' => Some('\u{0c}'),
1253        'v' => Some('\u{0b}'),
1254        '"' => Some('"'),
1255        '\'' => Some('\''),
1256        '\\' => Some('\\'),
1257        '&' => Some('&'),
1258        // DAML follows Haskell-style text escapes, including numeric escapes
1259        // (`\123`, `\o173`, `\x7B`) and named ASCII escapes (`\NUL`, `\SOH`,
1260        // ...). This lexer preserves source spans rather than fully decoding
1261        // multi-character escapes here, so accept the leading escape character
1262        // and let the remaining source characters flow through unchanged.
1263        '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
1264        _ => None,
1265    }
1266}
1267
1268impl TokenKind {
1269    /// The identifier text if this is an unqualified lowercase identifier —
1270    /// how the parser checks for (contextual) keywords.
1271    #[must_use]
1272    pub const fn keyword(&self) -> Option<&str> {
1273        match self {
1274            Self::LowerId {
1275                qualifier: None,
1276                name,
1277            } => Some(name.as_str()),
1278            _ => None,
1279        }
1280    }
1281
1282    #[must_use]
1283    pub fn is_keyword(&self, kw: &str) -> bool {
1284        self.keyword() == Some(kw)
1285    }
1286
1287    #[must_use]
1288    pub fn is_op(&self, op: &str) -> bool {
1289        matches!(self, Self::Op(o) if o.as_str() == op)
1290    }
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use super::*;
1296
1297    fn toks(src: &str) -> Vec<TokenKind> {
1298        let (tokens, errors) = lex(src).into_parts();
1299        assert!(errors.is_empty(), "lex errors: {errors:?}");
1300        tokens.into_iter().map(|t| t.kind).collect()
1301    }
1302
1303    fn lex_error_messages(src: &str) -> Vec<String> {
1304        let (_, errors) = lex(src).into_parts();
1305        errors.into_iter().map(|e| e.to_string()).collect()
1306    }
1307
1308    fn lower(name: &str) -> TokenKind {
1309        TokenKind::LowerId {
1310            qualifier: None,
1311            name: name.into(),
1312        }
1313    }
1314
1315    fn upper(name: &str) -> TokenKind {
1316        TokenKind::UpperId {
1317            qualifier: None,
1318            name: name.into(),
1319        }
1320    }
1321
1322    #[test]
1323    fn identifier_as_ref_str() {
1324        let identifier = Identifier::from("value");
1325        let operator = Operator::from("+");
1326        let module = ModuleName::from("DA.Map");
1327
1328        assert_eq!(identifier.as_ref(), "value");
1329        assert_eq!(operator.as_ref(), "+");
1330        assert_eq!(module.as_ref(), "DA.Map");
1331    }
1332
1333    #[test]
1334    fn line_comment_with_keywords_produces_no_tokens() {
1335        assert_eq!(toks("-- electing to exercise the option"), vec![]);
1336        assert_eq!(toks("--- template Foo"), vec![]);
1337    }
1338
1339    #[test]
1340    fn arrow_like_operator_is_not_comment() {
1341        assert_eq!(
1342            toks("a --> b"),
1343            vec![lower("a"), TokenKind::Op("-->".into()), lower("b")]
1344        );
1345    }
1346
1347    #[test]
1348    fn nested_block_comment() {
1349        assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
1350    }
1351
1352    #[test]
1353    fn string_with_keyword_and_escapes() {
1354        assert_eq!(
1355            toks(r#""template \"Foo\" \n""#),
1356            vec![TokenKind::StringLit("template \"Foo\" \n".into())]
1357        );
1358    }
1359
1360    #[test]
1361    fn qualified_identifiers() {
1362        assert_eq!(
1363            toks("DA.Set.fromList Map.Map foo"),
1364            vec![
1365                TokenKind::LowerId {
1366                    qualifier: Some("DA.Set".into()),
1367                    name: "fromList".into()
1368                },
1369                TokenKind::UpperId {
1370                    qualifier: Some("Map".into()),
1371                    name: "Map".into()
1372                },
1373                lower("foo"),
1374            ]
1375        );
1376    }
1377
1378    #[test]
1379    fn numbers() {
1380        assert_eq!(
1381            toks("42 1.5 0x1F 2e3 1_000"),
1382            vec![
1383                TokenKind::IntLit("42".into()),
1384                TokenKind::DecimalLit("1.5".into()),
1385                TokenKind::IntLit("0x1F".into()),
1386                TokenKind::DecimalLit("2e3".into()),
1387                TokenKind::IntLit("1_000".into()),
1388            ]
1389        );
1390    }
1391
1392    #[test]
1393    fn malformed_hex_literal_reports_error() {
1394        assert_eq!(
1395            lex_error_messages("0x 0x_"),
1396            vec![
1397                "hex literal requires at least one digit",
1398                "hex literal requires at least one digit",
1399            ]
1400        );
1401    }
1402
1403    #[test]
1404    fn malformed_decimal_exponent_reports_error() {
1405        assert_eq!(
1406            lex_error_messages("1e 1e+ 1e-"),
1407            vec![
1408                "decimal exponent requires at least one digit",
1409                "decimal exponent requires at least one digit",
1410                "decimal exponent requires at least one digit",
1411            ]
1412        );
1413    }
1414
1415    #[test]
1416    fn enum_from_to_is_not_decimal() {
1417        assert_eq!(
1418            toks("[1..5]"),
1419            vec![
1420                TokenKind::LBracket,
1421                TokenKind::IntLit("1".into()),
1422                TokenKind::Op("..".into()),
1423                TokenKind::IntLit("5".into()),
1424                TokenKind::RBracket,
1425            ]
1426        );
1427    }
1428
1429    #[test]
1430    fn primes_stay_in_identifier_and_char_lit_works() {
1431        assert_eq!(
1432            toks(r"foo' 'a' '\n'"),
1433            vec![
1434                lower("foo'"),
1435                TokenKind::CharLit("a".into()),
1436                TokenKind::CharLit("\n".into())
1437            ]
1438        );
1439    }
1440
1441    #[test]
1442    fn invalid_escape_sequences_report_errors() {
1443        assert_eq!(
1444            lex_error_messages(r#""\q" '\q'"#),
1445            vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
1446        );
1447    }
1448
1449    #[test]
1450    fn multi_character_char_literal_reports_error() {
1451        let (tokens, errors) = lex("'ab'").into_parts();
1452        assert_eq!(
1453            tokens.iter().map(|t| t.kind.clone()).collect::<Vec<_>>(),
1454            vec![TokenKind::CharLit("ab".into())]
1455        );
1456        assert_eq!(
1457            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
1458            vec!["character literal must contain exactly one character".to_string()]
1459        );
1460    }
1461
1462    #[test]
1463    fn operators_and_punctuation() {
1464        assert_eq!(
1465            toks("x <- f (y, z) `div` 2"),
1466            vec![
1467                lower("x"),
1468                TokenKind::Op("<-".into()),
1469                lower("f"),
1470                TokenKind::LParen,
1471                lower("y"),
1472                TokenKind::Comma,
1473                lower("z"),
1474                TokenKind::RParen,
1475                TokenKind::Backtick,
1476                lower("div"),
1477                TokenKind::Backtick,
1478                TokenKind::IntLit("2".into()),
1479            ]
1480        );
1481    }
1482
1483    #[test]
1484    fn spans_are_one_based() {
1485        let (tokens, _) = lex("ab\n  cd").into_parts();
1486        assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
1487        assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
1488    }
1489
1490    #[test]
1491    fn tab_advances_to_stop() {
1492        let (tokens, _) = lex("\tx").into_parts();
1493        assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
1494    }
1495
1496    #[test]
1497    fn unterminated_string_is_error_not_hang() {
1498        let (_, errors) = lex("x = \"oops\ny").into_parts();
1499        assert_eq!(errors.len(), 1);
1500    }
1501
1502    #[test]
1503    fn unterminated_block_comment_is_error_not_hang() {
1504        let (_, errors) = lex("{- never closed").into_parts();
1505        assert_eq!(errors.len(), 1);
1506    }
1507
1508    fn trivia_of(src: &str) -> Vec<Trivia> {
1509        let (_, trivia, _) = lex_with_trivia(src).into_parts();
1510        trivia
1511    }
1512
1513    /// The lossless oracle on one source: spans must tile the file and the
1514    /// reconstruction must be byte-identical.
1515    fn assert_round_trip(src: &str) {
1516        let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1517        assert!(errors.is_empty(), "lex errors: {errors:?}");
1518        assert_eq!(
1519            render_lossless(src, &tokens, &trivia).as_deref(),
1520            Ok(src),
1521            "round trip failed for {src:?}"
1522        );
1523    }
1524
1525    #[test]
1526    fn line_comment_becomes_trivia_with_exact_text_and_span() {
1527        let src = "x = 1 -- electing to exercise\ny = 2\n";
1528        let trivia = trivia_of(src);
1529        assert_eq!(trivia.len(), 1);
1530        assert_eq!(trivia[0].kind, TriviaKind::LineComment);
1531        assert_eq!(trivia[0].text, "-- electing to exercise");
1532        assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
1533        assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
1534    }
1535
1536    #[test]
1537    fn nested_block_comment_becomes_one_trivia() {
1538        let src = "{- outer {- inner -} still -} x";
1539        let trivia = trivia_of(src);
1540        assert_eq!(trivia.len(), 1);
1541        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1542        assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
1543    }
1544
1545    #[test]
1546    fn unterminated_block_comment_still_yields_trivia_to_eof() {
1547        let (_, trivia, errors) = lex_with_trivia("x {- never closed").into_parts();
1548        assert_eq!(errors.len(), 1);
1549        assert_eq!(trivia.len(), 1);
1550        assert_eq!(trivia[0].text, "{- never closed");
1551    }
1552
1553    #[test]
1554    fn blank_lines_between_items_counted() {
1555        let src = "x = 1\n\n\ny = 2\n";
1556        let trivia = trivia_of(src);
1557        assert_eq!(trivia.len(), 1);
1558        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
1559        assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
1560    }
1561
1562    #[test]
1563    fn blank_line_at_file_start_counted() {
1564        let trivia = trivia_of("\nx = 1\n");
1565        assert_eq!(trivia.len(), 1);
1566        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
1567        assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
1568    }
1569
1570    #[test]
1571    fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
1572        let src = "x = 1 {- a\n\nb -}\ny = 2\n";
1573        let trivia = trivia_of(src);
1574        assert_eq!(trivia.len(), 1, "{trivia:?}");
1575        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1576    }
1577
1578    #[test]
1579    fn blank_line_between_comments_counted() {
1580        let src = "-- a\n\n-- b\nx = 1\n";
1581        let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
1582        assert_eq!(
1583            kinds,
1584            vec![
1585                TriviaKind::LineComment,
1586                TriviaKind::BlankLines(1),
1587                TriviaKind::LineComment,
1588            ]
1589        );
1590    }
1591
1592    #[test]
1593    fn cpp_directive_becomes_trivia() {
1594        let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
1595        let trivia = trivia_of(src);
1596        assert_eq!(trivia.len(), 2);
1597        assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1598        assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1599    }
1600
1601    #[test]
1602    fn round_trip_is_byte_identical() {
1603        assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1604        assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1605        assert_round_trip("\tärger = [1..5] -- ütf\n");
1606        assert_round_trip("s = \"gap \\  \\ here\"\n");
1607        assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1608        assert_round_trip("\n\n  \nf = 1  \n   ");
1609        assert_round_trip("");
1610    }
1611
1612    #[test]
1613    fn render_lossless_detects_lost_bytes() {
1614        let src = "x = 1 -- comment\n";
1615        let (tokens, mut trivia, _) = lex_with_trivia(src).into_parts();
1616        trivia.clear(); // simulate a lexer that drops the comment
1617        assert!(render_lossless(src, &tokens, &trivia).is_err());
1618    }
1619
1620    #[test]
1621    fn unicode_identifier() {
1622        assert_eq!(
1623            toks("ärger = 1"),
1624            vec![
1625                lower("ärger"),
1626                TokenKind::Op("=".into()),
1627                TokenKind::IntLit("1".into())
1628            ]
1629        );
1630        let _ = upper("Ülf"); // helper used
1631    }
1632}