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