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]
465            .chars()
466            .next()
467            .expect("positive char_len_at guarantees one UTF-8 scalar");
468        i += len;
469
470        if escaped {
471            escaped = false;
472            continue;
473        }
474        if ch == '\\' {
475            escaped = true;
476            continue;
477        }
478        if ch == '\'' {
479            return i;
480        }
481    }
482
483    start + 1
484}
485
486fn end_hex_missing_digits(source: &str, start: usize) -> usize {
487    if start >= source.len() {
488        return start;
489    }
490    let after_prefix = start.saturating_add(2);
491    let prefix = source.get(start..after_prefix).unwrap_or("");
492    if prefix != "0x" && prefix != "0X" {
493        return start;
494    }
495
496    let mut end = after_prefix;
497    while let Some(len) = char_len_at(source, end) {
498        if source.get(end..end + len) != Some("_") {
499            break;
500        }
501        end += len;
502    }
503    end
504}
505
506fn end_decimal_exponent_missing_digits(source: &str, start: usize) -> usize {
507    let mut byte = start;
508    while byte < source.len() {
509        let len = match char_len_at(source, byte) {
510            Some(len) => len,
511            None => return start,
512        };
513        if len == 0 {
514            return start;
515        }
516        let ch = source[byte..byte + len]
517            .chars()
518            .next()
519            .expect("positive char_len_at guarantees one UTF-8 scalar");
520        if matches!(ch, 'e' | 'E') {
521            let mut end = byte + len;
522            if let Some(sign_len) = char_len_at(source, end) {
523                let sign = source[end..end + sign_len]
524                    .chars()
525                    .next()
526                    .expect("positive char_len_at guarantees one UTF-8 scalar");
527                if matches!(sign, '+' | '-') {
528                    end += sign_len;
529                }
530            }
531            return end;
532        }
533
534        if ch.is_whitespace() {
535            return start;
536        }
537        byte += len;
538    }
539
540    start
541}
542
543/// Byte offset of a 1-based (line, column) position.
544#[inline]
545fn byte_of_pos(source: &str, pos: Pos) -> usize {
546    let mut line = 1usize;
547    let mut col = 1usize;
548    for (idx, ch) in source.char_indices() {
549        if line == pos.line && col == pos.column {
550            return idx;
551        }
552        match ch {
553            '\n' => {
554                line += 1;
555                col = 1;
556            }
557            '\t' => col = ((col - 1) / TAB_STOP + 1) * TAB_STOP + 1,
558            _ => col += 1,
559        }
560    }
561    source.len()
562}
563
564#[derive(Debug, Clone, PartialEq, Eq)]
565#[non_exhaustive]
566pub enum LexErrorKind {
567    UnexpectedCharacter(char),
568    UnterminatedBlockComment,
569    UnterminatedStringLiteral,
570    UnterminatedStringGap,
571    InvalidEscapeSequence(char),
572    StraySingleQuote,
573    CharacterLiteralWrongLength,
574    HexLiteralMissingDigits,
575    DecimalExponentMissingDigits,
576}
577
578impl std::fmt::Display for LexErrorKind {
579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580        match self {
581            Self::UnexpectedCharacter(c) => write!(f, "unexpected character '{c}'"),
582            Self::UnterminatedBlockComment => f.write_str("unterminated block comment"),
583            Self::UnterminatedStringLiteral => f.write_str("unterminated string literal"),
584            Self::UnterminatedStringGap => f.write_str("unterminated string gap"),
585            Self::InvalidEscapeSequence(c) => write!(f, "invalid escape sequence \\{c}"),
586            Self::StraySingleQuote => f.write_str("stray single quote"),
587            Self::CharacterLiteralWrongLength => {
588                f.write_str("character literal must contain exactly one character")
589            }
590            Self::HexLiteralMissingDigits => f.write_str("hex literal requires at least one digit"),
591            Self::DecimalExponentMissingDigits => {
592                f.write_str("decimal exponent requires at least one digit")
593            }
594        }
595    }
596}
597
598#[derive(Debug, Clone, PartialEq, Eq)]
599pub struct LexOutput {
600    pub tokens: Vec<Token>,
601    pub errors: Vec<LexError>,
602}
603
604impl LexOutput {
605    #[must_use]
606    pub fn into_parts(self) -> (Vec<Token>, Vec<LexError>) {
607        (self.tokens, self.errors)
608    }
609}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
612pub struct LexWithTriviaOutput {
613    pub tokens: Vec<Token>,
614    pub trivia: Vec<Trivia>,
615    pub errors: Vec<LexError>,
616}
617
618impl LexWithTriviaOutput {
619    #[must_use]
620    pub fn into_parts(self) -> (Vec<Token>, Vec<Trivia>, Vec<LexError>) {
621        (self.tokens, self.trivia, self.errors)
622    }
623}
624
625#[derive(Debug, Clone, PartialEq, Eq)]
626#[non_exhaustive]
627pub enum RenderLosslessError {
628    OverlappingSpans {
629        start: usize,
630    },
631    UncoveredBytes {
632        start: usize,
633        end: usize,
634        text: String,
635    },
636    UncoveredTail {
637        start: usize,
638        text: String,
639    },
640}
641
642impl std::fmt::Display for RenderLosslessError {
643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644        match self {
645            Self::OverlappingSpans { start } => write!(f, "overlapping spans at byte {start}"),
646            Self::UncoveredBytes { start, end, text } => write!(
647                f,
648                "bytes {start}..{end} lost (not covered by any token/trivia): {text:?}"
649            ),
650            Self::UncoveredTail { start, text } => {
651                write!(f, "bytes {start}.. lost at EOF: {text:?}")
652            }
653        }
654    }
655}
656
657impl std::error::Error for RenderLosslessError {}
658
659const fn is_symbol_char(c: char) -> bool {
660    matches!(
661        c,
662        '!' | '#'
663            | '$'
664            | '%'
665            | '&'
666            | '*'
667            | '+'
668            | '.'
669            | '/'
670            | '<'
671            | '='
672            | '>'
673            | '?'
674            | '@'
675            | '\\'
676            | '^'
677            | '|'
678            | '-'
679            | '~'
680            | ':'
681    )
682}
683
684fn is_ident_start(c: char) -> bool {
685    c.is_alphabetic() || c == '_'
686}
687
688fn is_ident_char(c: char) -> bool {
689    c.is_alphanumeric() || c == '_' || c == '\''
690}
691
692pub(crate) const TAB_STOP: usize = 8;
693
694struct Lexer<'a> {
695    chars: Vec<char>,
696    src: &'a str,
697    i: usize,
698    byte: usize,
699    line: usize,
700    column: usize,
701    capture_trivia: bool,
702    tokens: Vec<Token>,
703    trivia: Vec<Trivia>,
704    errors: Vec<LexError>,
705}
706
707impl<'a> Lexer<'a> {
708    fn new(source: &'a str, capture_trivia: bool) -> Self {
709        Self {
710            chars: source.chars().collect(),
711            src: source,
712            i: 0,
713            byte: 0,
714            line: 1,
715            column: 1,
716            capture_trivia,
717            tokens: Vec::new(),
718            trivia: Vec::new(),
719            errors: Vec::new(),
720        }
721    }
722}
723
724/// Lex `source` into tokens and lexical errors only.
725///
726/// Skips trivia allocation and blank-line processing. Use [`lex_with_trivia`]
727/// when callers also need comments and trivia for lossless rendering.
728#[must_use]
729pub fn lex(source: &str) -> LexOutput {
730    let mut lexer = Lexer::new(source, false);
731    lexer.scan_tokens();
732    LexOutput {
733        tokens: lexer.tokens,
734        errors: lexer.errors,
735    }
736}
737
738/// Lex `source` into tokens, trivia, and lexical errors.
739#[must_use]
740pub fn lex_with_trivia(source: &str) -> LexWithTriviaOutput {
741    let mut lexer = Lexer::new(source, true);
742    lexer.scan_tokens();
743    let mut trivia = lexer.trivia;
744    add_blank_line_trivia(source, &lexer.tokens, &mut trivia);
745    trivia.sort_by_key(|t| t.start);
746    LexWithTriviaOutput {
747        tokens: lexer.tokens,
748        trivia,
749        errors: lexer.errors,
750    }
751}
752
753/// Reconstruct the source from token and trivia spans.
754///
755/// `Ok` only when the spans tile the file — every non-whitespace byte inside
756/// exactly one token or comment span — in which case the result is
757/// byte-identical to `source`. This is the lossless-trivia oracle for the
758/// formatter.
759#[must_use = "handle render errors instead of discarding"]
760pub fn render_lossless(
761    source: &str,
762    tokens: &[Token],
763    trivia: &[Trivia],
764) -> Result<String, RenderLosslessError> {
765    let mut items: Vec<(usize, usize)> = tokens
766        .iter()
767        .filter(|t| {
768            !matches!(
769                t.kind,
770                TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
771            )
772        })
773        .map(|t| (t.start, t.end))
774        .chain(
775            trivia
776                .iter()
777                .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
778                .map(|t| (t.start, t.end)),
779        )
780        .collect();
781    items.sort_unstable();
782    let mut out = String::with_capacity(source.len());
783    let mut prev = 0usize;
784    for (start, end) in items {
785        if start < prev {
786            return Err(RenderLosslessError::OverlappingSpans { start });
787        }
788        let gap = &source[prev..start];
789        if !gap.chars().all(char::is_whitespace) {
790            return Err(RenderLosslessError::UncoveredBytes {
791                start: prev,
792                end: start,
793                text: gap.to_string(),
794            });
795        }
796        out.push_str(gap);
797        out.push_str(&source[start..end]);
798        prev = end;
799    }
800    let tail = &source[prev..];
801    if !tail.chars().all(char::is_whitespace) {
802        return Err(RenderLosslessError::UncoveredTail {
803            start: prev,
804            text: tail.to_string(),
805        });
806    }
807    out.push_str(tail);
808    Ok(out)
809}
810
811/// A blank line is a whitespace-only line lying entirely between spans (so a
812/// blank-looking line inside a block comment or multiline string does not
813/// count). Emitted as one `BlankLines(n)` per maximal run so the printer can
814/// preserve paragraph breaks.
815fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
816    let mut spans: Vec<(usize, usize)> = tokens
817        .iter()
818        .map(|t| (t.start, t.end))
819        .chain(trivia.iter().map(|t| (t.start, t.end)))
820        .collect();
821    spans.sort_unstable();
822    let bytes = source.as_bytes();
823    let mut blanks = Vec::new();
824    let mut gap_start = 0usize;
825    let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
826        // Newline offsets inside the gap. Full lines between two newlines
827        // (or before the first newline when the gap starts the file) are
828        // blank by construction: the gap holds no token/comment bytes, and
829        // any stray non-whitespace there fails the lossless render anyway.
830        let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
831        // Interior gap: the partial line before the first newline belongs to
832        // the preceding token's line, so only lines between newlines count.
833        // A gap at byte 0 has no such partial line — line 1 itself is blank.
834        let count = if from == 0 {
835            newlines.len()
836        } else {
837            newlines.len().saturating_sub(1)
838        };
839        if count == 0 {
840            return;
841        }
842        let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
843        let region_end = newlines[newlines.len() - 1] + 1;
844        let line = source[..region_start].matches('\n').count() + 1;
845        out.push(Trivia {
846            kind: TriviaKind::BlankLines(count),
847            text: String::new(),
848            pos: Pos { line, column: 1 },
849            start: region_start,
850            end: region_end,
851        });
852    };
853    for &(s, e) in &spans {
854        if s > gap_start {
855            emit_gap(gap_start, s, &mut blanks);
856        }
857        gap_start = gap_start.max(e);
858    }
859    if source.len() > gap_start {
860        emit_gap(gap_start, source.len(), &mut blanks);
861    }
862    trivia.extend(blanks);
863}
864
865impl<'a> Lexer<'a> {
866    fn peek(&self) -> Option<char> {
867        self.chars.get(self.i).copied()
868    }
869
870    fn peek_at(&self, n: usize) -> Option<char> {
871        self.chars.get(self.i + n).copied()
872    }
873
874    fn bump(&mut self) -> Option<char> {
875        let c = self.chars.get(self.i).copied()?;
876        self.i += 1;
877        self.byte += c.len_utf8();
878        match c {
879            '\n' => {
880                self.line += 1;
881                self.column = 1;
882            }
883            '\t' => {
884                // Tab advances to the next multiple-of-8 stop, matching GHC,
885                // so mixed tabs/spaces don't silently corrupt layout.
886                self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
887            }
888            _ => self.column += 1,
889        }
890        Some(c)
891    }
892
893    /// Next char when `peek` / `peek_at` already confirmed input remains.
894    fn bump_after_peek(&mut self) -> char {
895        self.bump()
896            .expect("lexer cursor guarded by peek/peek_at before bump")
897    }
898
899    const fn pos(&self) -> Pos {
900        Pos {
901            line: self.line,
902            column: self.column,
903        }
904    }
905
906    /// `start` is the token's first byte; its end is wherever the cursor is
907    /// now, so call this immediately after consuming the token.
908    fn push(&mut self, tok: TokenKind, pos: Pos, start: usize) {
909        self.tokens.push(Token {
910            kind: tok,
911            pos,
912            start,
913            end: self.byte,
914        });
915    }
916
917    fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
918        if !self.capture_trivia {
919            return;
920        }
921        self.trivia.push(Trivia {
922            kind,
923            text: self.src[start..self.byte].to_string(),
924            pos,
925            start,
926            end: self.byte,
927        });
928    }
929
930    fn error(&mut self, kind: LexErrorKind, pos: Pos) {
931        self.errors.push(LexError { kind, pos });
932    }
933
934    fn scan_tokens(&mut self) {
935        while let Some(c) = self.peek() {
936            let pos = self.pos();
937            let start = self.byte;
938            match c {
939                ' ' | '\t' | '\n' | '\r' => {
940                    self.bump();
941                }
942                '(' => {
943                    self.bump();
944                    self.push(TokenKind::LParen, pos, start);
945                }
946                ')' => {
947                    self.bump();
948                    self.push(TokenKind::RParen, pos, start);
949                }
950                '[' => {
951                    self.bump();
952                    self.push(TokenKind::LBracket, pos, start);
953                }
954                ']' => {
955                    self.bump();
956                    self.push(TokenKind::RBracket, pos, start);
957                }
958                ',' => {
959                    self.bump();
960                    self.push(TokenKind::Comma, pos, start);
961                }
962                ';' => {
963                    self.bump();
964                    self.push(TokenKind::Semi, pos, start);
965                }
966                '`' => {
967                    self.bump();
968                    self.push(TokenKind::Backtick, pos, start);
969                }
970                '{' => {
971                    if self.peek_at(1) == Some('-') {
972                        self.block_comment(pos);
973                    } else {
974                        self.bump();
975                        self.push(TokenKind::LBrace, pos, start);
976                    }
977                }
978                '}' => {
979                    self.bump();
980                    self.push(TokenKind::RBrace, pos, start);
981                }
982                // CPP preprocessor directive (#ifdef/#endif/#include...) at
983                // column 1 — daml-prim/stdlib sources use {-# LANGUAGE CPP #-};
984                // directives are line-based, skip the whole line.
985                '#' if self.column == 1
986                    && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
987                {
988                    while self.peek().is_some_and(|c| c != '\n') {
989                        self.bump();
990                    }
991                    self.push_trivia(TriviaKind::CppDirective, pos, start);
992                }
993                '"' => self.string_lit(pos),
994                '\'' => self.char_lit(pos),
995                c if c.is_ascii_digit() => self.number(pos),
996                c if is_ident_start(c) => self.identifier(pos),
997                c if is_symbol_char(c) => self.operator(pos),
998                _ => {
999                    self.bump();
1000                    self.error(LexErrorKind::UnexpectedCharacter(c), pos);
1001                }
1002            }
1003        }
1004    }
1005
1006    /// `{- ... -}`, nested as in Haskell. Unterminated comment is an error
1007    /// but consumes to EOF (no hang, no panic).
1008    fn block_comment(&mut self, pos: Pos) {
1009        let start = self.byte;
1010        self.bump(); // {
1011        self.bump(); // -
1012        let mut depth = 1usize;
1013        while depth > 0 {
1014            match self.peek() {
1015                None => {
1016                    self.error(LexErrorKind::UnterminatedBlockComment, pos);
1017                    self.push_trivia(TriviaKind::BlockComment, pos, start);
1018                    return;
1019                }
1020                Some('{') if self.peek_at(1) == Some('-') => {
1021                    self.bump();
1022                    self.bump();
1023                    depth += 1;
1024                }
1025                Some('-') if self.peek_at(1) == Some('}') => {
1026                    self.bump();
1027                    self.bump();
1028                    depth -= 1;
1029                }
1030                Some(_) => {
1031                    self.bump();
1032                }
1033            }
1034        }
1035        self.push_trivia(TriviaKind::BlockComment, pos, start);
1036    }
1037
1038    fn string_lit(&mut self, pos: Pos) {
1039        let start = self.byte;
1040        self.bump(); // opening "
1041        let mut value = String::new();
1042        loop {
1043            match self.peek() {
1044                None | Some('\n') => {
1045                    self.error(LexErrorKind::UnterminatedStringLiteral, pos);
1046                    break;
1047                }
1048                Some('"') => {
1049                    self.bump();
1050                    break;
1051                }
1052                Some('\\') => {
1053                    let escape_pos = self.pos();
1054                    self.bump();
1055                    match self.peek() {
1056                        // String gap: backslash, whitespace, backslash.
1057                        Some(w) if w.is_whitespace() => {
1058                            while self.peek().is_some_and(|c| c.is_whitespace()) {
1059                                self.bump();
1060                            }
1061                            if self.peek() == Some('\\') {
1062                                self.bump();
1063                            } else {
1064                                self.error(LexErrorKind::UnterminatedStringGap, pos);
1065                                break;
1066                            }
1067                        }
1068                        Some(e) => {
1069                            self.bump();
1070                            match unescape(e) {
1071                                Some(c) => value.push(c),
1072                                None => {
1073                                    self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
1074                                    value.push(e);
1075                                }
1076                            }
1077                        }
1078                        None => {
1079                            self.error(LexErrorKind::UnterminatedStringLiteral, pos);
1080                            break;
1081                        }
1082                    }
1083                }
1084                Some(c) => {
1085                    self.bump();
1086                    value.push(c);
1087                }
1088            }
1089        }
1090        self.push(TokenKind::StringLit(value), pos, start);
1091    }
1092
1093    /// `'a'`, `'\n'`, `'\x41'`. A lone `'` that doesn't close within a few
1094    /// chars is not a char literal (identifiers consume their own primes, so
1095    /// this only triggers at expression positions).
1096    fn char_lit(&mut self, pos: Pos) {
1097        let start = self.byte;
1098        // Lookahead: find closing quote within a short window.
1099        let mut j = self.i + 1;
1100        let mut escaped = false;
1101        let mut ok = false;
1102        let window_end = (self.i + 12).min(self.chars.len());
1103        while j < window_end {
1104            match self.chars[j] {
1105                '\\' if !escaped => escaped = true,
1106                '\'' if !escaped => {
1107                    ok = j > self.i + 1;
1108                    break;
1109                }
1110                '\n' => break,
1111                _ => escaped = false,
1112            }
1113            j += 1;
1114        }
1115        if !ok {
1116            self.bump();
1117            self.error(LexErrorKind::StraySingleQuote, pos);
1118            return;
1119        }
1120        self.bump(); // opening '
1121        let mut value = String::new();
1122        while self.peek() != Some('\'') {
1123            let c = self.bump_after_peek();
1124            if c == '\\' {
1125                let escape_pos = Pos {
1126                    line: self.line,
1127                    column: self.column.saturating_sub(1),
1128                };
1129                if let Some(e) = self.bump() {
1130                    match unescape(e) {
1131                        Some(c) => value.push(c),
1132                        None => {
1133                            self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
1134                            value.push(e);
1135                        }
1136                    }
1137                }
1138            } else {
1139                value.push(c);
1140            }
1141        }
1142        self.bump(); // closing '
1143        if value.chars().count() != 1 {
1144            self.error(LexErrorKind::CharacterLiteralWrongLength, pos);
1145        }
1146        self.push(TokenKind::CharLit(value), pos, start);
1147    }
1148
1149    fn number(&mut self, pos: Pos) {
1150        let start = self.byte;
1151        let mut text = String::new();
1152        if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
1153            text.push(self.bump_after_peek());
1154            text.push(self.bump_after_peek());
1155            let mut has_hex_digit = false;
1156            while self
1157                .peek()
1158                .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
1159            {
1160                let c = self.bump_after_peek();
1161                has_hex_digit |= c.is_ascii_hexdigit();
1162                text.push(c);
1163            }
1164            if !has_hex_digit {
1165                self.error(LexErrorKind::HexLiteralMissingDigits, pos);
1166            }
1167            self.push(TokenKind::IntLit(text), pos, start);
1168            return;
1169        }
1170        while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
1171            text.push(self.bump_after_peek());
1172        }
1173        let mut decimal = false;
1174        // `1.5` is a decimal but `1..5` or `1.foo` is not.
1175        if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
1176            decimal = true;
1177            text.push(self.bump_after_peek());
1178            while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
1179                text.push(self.bump_after_peek());
1180            }
1181        }
1182        if matches!(self.peek(), Some('e' | 'E')) {
1183            decimal = true;
1184            if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
1185                || (matches!(self.peek_at(1), Some('+' | '-'))
1186                    && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
1187            {
1188                text.push(self.bump_after_peek());
1189                if matches!(self.peek(), Some('+' | '-')) {
1190                    text.push(self.bump_after_peek());
1191                }
1192                while self.peek().is_some_and(|c| c.is_ascii_digit()) {
1193                    text.push(self.bump_after_peek());
1194                }
1195            } else {
1196                text.push(self.bump_after_peek());
1197                if matches!(self.peek(), Some('+' | '-')) {
1198                    text.push(self.bump_after_peek());
1199                }
1200                self.error(LexErrorKind::DecimalExponentMissingDigits, pos);
1201            }
1202        }
1203        if decimal {
1204            self.push(TokenKind::DecimalLit(text), pos, start);
1205        } else {
1206            self.push(TokenKind::IntLit(text), pos, start);
1207        }
1208    }
1209
1210    /// Identifiers, with greedy qualification: `DA.Set.fromList` is one
1211    /// token (qualifier "DA.Set", name "fromList").
1212    fn identifier(&mut self, pos: Pos) {
1213        let start = self.byte;
1214        let mut segments: Vec<String> = Vec::new();
1215        loop {
1216            let mut seg = String::new();
1217            while self.peek().is_some_and(is_ident_char) {
1218                seg.push(self.bump_after_peek());
1219            }
1220            let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
1221            segments.push(seg);
1222            // Continue qualification only after an Upper segment: `Foo.bar`
1223            // is qualified, `foo.bar` is composition/projection.
1224            if seg_is_upper
1225                && self.peek() == Some('.')
1226                && self.peek_at(1).is_some_and(is_ident_start)
1227            {
1228                self.bump(); // .
1229                continue;
1230            }
1231            break;
1232        }
1233        let name = segments
1234            .pop()
1235            .expect("identifier loop always records at least one segment");
1236        let qualifier = if segments.is_empty() {
1237            None
1238        } else {
1239            Some(segments.join(".").into())
1240        };
1241        let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
1242            TokenKind::UpperId {
1243                qualifier,
1244                name: name.into(),
1245            }
1246        } else {
1247            TokenKind::LowerId {
1248                qualifier,
1249                name: name.into(),
1250            }
1251        };
1252        self.push(tok, pos, start);
1253    }
1254
1255    fn operator(&mut self, pos: Pos) {
1256        let start = self.i;
1257        let byte_start = self.byte;
1258        while self.peek().is_some_and(is_symbol_char) {
1259            // `{-` inside an operator run can't happen ({ isn't a symbol
1260            // char), but `--` comment detection needs the full run first.
1261            self.bump();
1262        }
1263        let text: String = self.chars[start..self.i].iter().collect();
1264        // A run of 2+ dashes and nothing else is a line comment (Haskell
1265        // rule: `-->` is an operator, `--` and `---` start comments).
1266        if text.len() >= 2 && text.chars().all(|c| c == '-') {
1267            while self.peek().is_some_and(|c| c != '\n') {
1268                self.bump();
1269            }
1270            self.push_trivia(TriviaKind::LineComment, pos, byte_start);
1271            return;
1272        }
1273        self.push(TokenKind::Op(text.into()), pos, byte_start);
1274    }
1275}
1276
1277const fn unescape(c: char) -> Option<char> {
1278    match c {
1279        'n' => Some('\n'),
1280        't' => Some('\t'),
1281        'r' => Some('\r'),
1282        '0' => Some('\0'),
1283        'a' => Some('\u{07}'),
1284        'b' => Some('\u{08}'),
1285        'f' => Some('\u{0c}'),
1286        'v' => Some('\u{0b}'),
1287        '"' => Some('"'),
1288        '\'' => Some('\''),
1289        '\\' => Some('\\'),
1290        '&' => Some('&'),
1291        // DAML follows Haskell-style text escapes, including numeric escapes
1292        // (`\123`, `\o173`, `\x7B`) and named ASCII escapes (`\NUL`, `\SOH`,
1293        // ...). This lexer preserves source spans rather than fully decoding
1294        // multi-character escapes here, so accept the leading escape character
1295        // and let the remaining source characters flow through unchanged.
1296        '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
1297        _ => None,
1298    }
1299}
1300
1301impl TokenKind {
1302    /// The identifier text if this is an unqualified lowercase identifier —
1303    /// how the parser checks for (contextual) keywords.
1304    #[must_use]
1305    pub const fn keyword(&self) -> Option<&str> {
1306        match self {
1307            Self::LowerId {
1308                qualifier: None,
1309                name,
1310            } => Some(name.as_str()),
1311            _ => None,
1312        }
1313    }
1314
1315    #[must_use]
1316    pub fn is_keyword(&self, kw: &str) -> bool {
1317        self.keyword() == Some(kw)
1318    }
1319
1320    #[must_use]
1321    pub fn is_op(&self, op: &str) -> bool {
1322        matches!(self, Self::Op(o) if o.as_str() == op)
1323    }
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328    use super::*;
1329
1330    fn toks(src: &str) -> Vec<TokenKind> {
1331        let (tokens, errors) = lex(src).into_parts();
1332        assert!(errors.is_empty(), "lex errors: {errors:?}");
1333        tokens.into_iter().map(|t| t.kind).collect()
1334    }
1335
1336    fn lex_error_messages(src: &str) -> Vec<String> {
1337        let (_, errors) = lex(src).into_parts();
1338        errors.into_iter().map(|e| e.to_string()).collect()
1339    }
1340
1341    fn lower(name: &str) -> TokenKind {
1342        TokenKind::LowerId {
1343            qualifier: None,
1344            name: name.into(),
1345        }
1346    }
1347
1348    fn upper(name: &str) -> TokenKind {
1349        TokenKind::UpperId {
1350            qualifier: None,
1351            name: name.into(),
1352        }
1353    }
1354
1355    #[test]
1356    fn identifier_as_ref_str() {
1357        let identifier = Identifier::from("value");
1358        let operator = Operator::from("+");
1359        let module = ModuleName::from("DA.Map");
1360
1361        assert_eq!(identifier.as_ref(), "value");
1362        assert_eq!(operator.as_ref(), "+");
1363        assert_eq!(module.as_ref(), "DA.Map");
1364    }
1365
1366    #[test]
1367    fn line_comment_with_keywords_produces_no_tokens() {
1368        assert_eq!(toks("-- electing to exercise the option"), vec![]);
1369        assert_eq!(toks("--- template Foo"), vec![]);
1370    }
1371
1372    #[test]
1373    fn arrow_like_operator_is_not_comment() {
1374        assert_eq!(
1375            toks("a --> b"),
1376            vec![lower("a"), TokenKind::Op("-->".into()), lower("b")]
1377        );
1378    }
1379
1380    #[test]
1381    fn nested_block_comment() {
1382        assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
1383    }
1384
1385    #[test]
1386    fn string_with_keyword_and_escapes() {
1387        assert_eq!(
1388            toks(r#""template \"Foo\" \n""#),
1389            vec![TokenKind::StringLit("template \"Foo\" \n".into())]
1390        );
1391    }
1392
1393    #[test]
1394    fn qualified_identifiers() {
1395        assert_eq!(
1396            toks("DA.Set.fromList Map.Map foo"),
1397            vec![
1398                TokenKind::LowerId {
1399                    qualifier: Some("DA.Set".into()),
1400                    name: "fromList".into()
1401                },
1402                TokenKind::UpperId {
1403                    qualifier: Some("Map".into()),
1404                    name: "Map".into()
1405                },
1406                lower("foo"),
1407            ]
1408        );
1409    }
1410
1411    #[test]
1412    fn numbers() {
1413        assert_eq!(
1414            toks("42 1.5 0x1F 2e3 1_000"),
1415            vec![
1416                TokenKind::IntLit("42".into()),
1417                TokenKind::DecimalLit("1.5".into()),
1418                TokenKind::IntLit("0x1F".into()),
1419                TokenKind::DecimalLit("2e3".into()),
1420                TokenKind::IntLit("1_000".into()),
1421            ]
1422        );
1423    }
1424
1425    #[test]
1426    fn malformed_hex_literal_reports_error() {
1427        assert_eq!(
1428            lex_error_messages("0x 0x_"),
1429            vec![
1430                "hex literal requires at least one digit",
1431                "hex literal requires at least one digit",
1432            ]
1433        );
1434    }
1435
1436    #[test]
1437    fn malformed_decimal_exponent_reports_error() {
1438        assert_eq!(
1439            lex_error_messages("1e 1e+ 1e-"),
1440            vec![
1441                "decimal exponent requires at least one digit",
1442                "decimal exponent requires at least one digit",
1443                "decimal exponent requires at least one digit",
1444            ]
1445        );
1446    }
1447
1448    #[test]
1449    fn enum_from_to_is_not_decimal() {
1450        assert_eq!(
1451            toks("[1..5]"),
1452            vec![
1453                TokenKind::LBracket,
1454                TokenKind::IntLit("1".into()),
1455                TokenKind::Op("..".into()),
1456                TokenKind::IntLit("5".into()),
1457                TokenKind::RBracket,
1458            ]
1459        );
1460    }
1461
1462    #[test]
1463    fn primes_stay_in_identifier_and_char_lit_works() {
1464        assert_eq!(
1465            toks(r"foo' 'a' '\n'"),
1466            vec![
1467                lower("foo'"),
1468                TokenKind::CharLit("a".into()),
1469                TokenKind::CharLit("\n".into())
1470            ]
1471        );
1472    }
1473
1474    #[test]
1475    fn invalid_escape_sequences_report_errors() {
1476        assert_eq!(
1477            lex_error_messages(r#""\q" '\q'"#),
1478            vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
1479        );
1480    }
1481
1482    #[test]
1483    fn multi_character_char_literal_reports_error() {
1484        let (tokens, errors) = lex("'ab'").into_parts();
1485        assert_eq!(
1486            tokens.iter().map(|t| t.kind.clone()).collect::<Vec<_>>(),
1487            vec![TokenKind::CharLit("ab".into())]
1488        );
1489        assert_eq!(
1490            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
1491            vec!["character literal must contain exactly one character".to_string()]
1492        );
1493    }
1494
1495    #[test]
1496    fn operators_and_punctuation() {
1497        assert_eq!(
1498            toks("x <- f (y, z) `div` 2"),
1499            vec![
1500                lower("x"),
1501                TokenKind::Op("<-".into()),
1502                lower("f"),
1503                TokenKind::LParen,
1504                lower("y"),
1505                TokenKind::Comma,
1506                lower("z"),
1507                TokenKind::RParen,
1508                TokenKind::Backtick,
1509                lower("div"),
1510                TokenKind::Backtick,
1511                TokenKind::IntLit("2".into()),
1512            ]
1513        );
1514    }
1515
1516    #[test]
1517    fn spans_are_one_based() {
1518        let (tokens, _) = lex("ab\n  cd").into_parts();
1519        assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
1520        assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
1521    }
1522
1523    #[test]
1524    fn tab_advances_to_stop() {
1525        let (tokens, _) = lex("\tx").into_parts();
1526        assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
1527    }
1528
1529    #[test]
1530    fn unterminated_string_is_error_not_hang() {
1531        let (_, errors) = lex("x = \"oops\ny").into_parts();
1532        assert_eq!(errors.len(), 1);
1533    }
1534
1535    #[test]
1536    fn unterminated_block_comment_is_error_not_hang() {
1537        let (_, errors) = lex("{- never closed").into_parts();
1538        assert_eq!(errors.len(), 1);
1539    }
1540
1541    fn trivia_of(src: &str) -> Vec<Trivia> {
1542        let (_, trivia, _) = lex_with_trivia(src).into_parts();
1543        trivia
1544    }
1545
1546    /// The lossless oracle on one source: spans must tile the file and the
1547    /// reconstruction must be byte-identical.
1548    fn assert_round_trip(src: &str) {
1549        let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1550        assert!(errors.is_empty(), "lex errors: {errors:?}");
1551        assert_eq!(
1552            render_lossless(src, &tokens, &trivia).as_deref(),
1553            Ok(src),
1554            "round trip failed for {src:?}"
1555        );
1556    }
1557
1558    #[test]
1559    fn line_comment_becomes_trivia_with_exact_text_and_span() {
1560        let src = "x = 1 -- electing to exercise\ny = 2\n";
1561        let trivia = trivia_of(src);
1562        assert_eq!(trivia.len(), 1);
1563        assert_eq!(trivia[0].kind, TriviaKind::LineComment);
1564        assert_eq!(trivia[0].text, "-- electing to exercise");
1565        assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
1566        assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
1567    }
1568
1569    #[test]
1570    fn nested_block_comment_becomes_one_trivia() {
1571        let src = "{- outer {- inner -} still -} x";
1572        let trivia = trivia_of(src);
1573        assert_eq!(trivia.len(), 1);
1574        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1575        assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
1576    }
1577
1578    #[test]
1579    fn unterminated_block_comment_still_yields_trivia_to_eof() {
1580        let (_, trivia, errors) = lex_with_trivia("x {- never closed").into_parts();
1581        assert_eq!(errors.len(), 1);
1582        assert_eq!(trivia.len(), 1);
1583        assert_eq!(trivia[0].text, "{- never closed");
1584    }
1585
1586    #[test]
1587    fn blank_lines_between_items_counted() {
1588        let src = "x = 1\n\n\ny = 2\n";
1589        let trivia = trivia_of(src);
1590        assert_eq!(trivia.len(), 1);
1591        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
1592        assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
1593    }
1594
1595    #[test]
1596    fn blank_line_at_file_start_counted() {
1597        let trivia = trivia_of("\nx = 1\n");
1598        assert_eq!(trivia.len(), 1);
1599        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
1600        assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
1601    }
1602
1603    #[test]
1604    fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
1605        let src = "x = 1 {- a\n\nb -}\ny = 2\n";
1606        let trivia = trivia_of(src);
1607        assert_eq!(trivia.len(), 1, "{trivia:?}");
1608        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1609    }
1610
1611    #[test]
1612    fn blank_line_between_comments_counted() {
1613        let src = "-- a\n\n-- b\nx = 1\n";
1614        let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
1615        assert_eq!(
1616            kinds,
1617            vec![
1618                TriviaKind::LineComment,
1619                TriviaKind::BlankLines(1),
1620                TriviaKind::LineComment,
1621            ]
1622        );
1623    }
1624
1625    #[test]
1626    fn cpp_directive_becomes_trivia() {
1627        let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
1628        let trivia = trivia_of(src);
1629        assert_eq!(trivia.len(), 2);
1630        assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1631        assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1632    }
1633
1634    #[test]
1635    fn round_trip_is_byte_identical() {
1636        assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1637        assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1638        assert_round_trip("\tärger = [1..5] -- ütf\n");
1639        assert_round_trip("s = \"gap \\  \\ here\"\n");
1640        assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1641        assert_round_trip("\n\n  \nf = 1  \n   ");
1642        assert_round_trip("");
1643    }
1644
1645    #[test]
1646    fn render_lossless_detects_lost_bytes() {
1647        let src = "x = 1 -- comment\n";
1648        let (tokens, mut trivia, _) = lex_with_trivia(src).into_parts();
1649        trivia.clear(); // simulate a lexer that drops the comment
1650        assert!(render_lossless(src, &tokens, &trivia).is_err());
1651    }
1652
1653    #[test]
1654    fn unicode_identifier() {
1655        assert_eq!(
1656            toks("ärger = 1"),
1657            vec![
1658                lower("ärger"),
1659                TokenKind::Op("=".into()),
1660                TokenKind::IntLit("1".into())
1661            ]
1662        );
1663        let _ = upper("Ülf"); // helper used
1664    }
1665
1666    /// Parse-only `lex` must emit the same tokens as `lex_with_trivia`; only
1667    /// trivia differs. Comments and blank lines must not change tokenization.
1668    fn assert_lex_tokens_match(src: &str) {
1669        let (parse_tokens, parse_errors) = lex(src).into_parts();
1670        let (trivia_tokens, _, trivia_errors) = lex_with_trivia(src).into_parts();
1671        assert_eq!(parse_errors, trivia_errors, "lex errors differ for {src:?}");
1672        assert_eq!(
1673            parse_tokens.len(),
1674            trivia_tokens.len(),
1675            "token count for {src:?}"
1676        );
1677        for (a, b) in parse_tokens.iter().zip(trivia_tokens.iter()) {
1678            assert_eq!(a.kind, b.kind, "token kind for {src:?}");
1679            assert_eq!(a.pos, b.pos, "token pos for {src:?}");
1680            assert_eq!(a.start, b.start, "token start for {src:?}");
1681            assert_eq!(a.end, b.end, "token end for {src:?}");
1682        }
1683    }
1684
1685    #[test]
1686    fn lex_and_lex_with_trivia_emit_identical_tokens() {
1687        assert_lex_tokens_match("x = 1 -- electing to exercise\ny = 2\n");
1688        assert_lex_tokens_match("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1689        assert_lex_tokens_match("{- outer {- inner -} still -} x");
1690        assert_lex_tokens_match("#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n");
1691        assert_lex_tokens_match("\n\n  \nf = 1  \n   ");
1692    }
1693
1694    #[test]
1695    fn lex_with_trivia_preserves_lossless_comment_and_blank_line_render() {
1696        let sources = [
1697            "x = 1 -- electing to exercise\ny = 2\n",
1698            "x = 1\n\n\ny = 2\n",
1699            "-- a\n\n-- b\nx = 1\n",
1700        ];
1701        for src in sources {
1702            let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1703            assert!(errors.is_empty(), "lex errors for {src:?}: {errors:?}");
1704            assert_eq!(
1705                render_lossless(src, &tokens, &trivia).as_deref(),
1706                Ok(src),
1707                "lossless render failed for {src:?}"
1708            );
1709        }
1710    }
1711}