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#[must_use = "handle render errors instead of discarding"]
928pub fn render_lossless(
929    source: &str,
930    tokens: &[Token],
931    trivia: &[Trivia],
932) -> Result<String, RenderLosslessError> {
933    let mut items: Vec<(usize, usize)> = tokens
934        .iter()
935        .filter(|t| {
936            !matches!(
937                t.kind,
938                TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
939            )
940        })
941        .map(|t| (t.start, t.end))
942        .chain(
943            trivia
944                .iter()
945                .filter(|t| !matches!(t.kind, TriviaKind::BlankLines(_)))
946                .map(|t| (t.start, t.end)),
947        )
948        .collect();
949    items.sort_unstable();
950    let mut out = String::with_capacity(source.len());
951    let mut prev = 0usize;
952    for (start, end) in items {
953        if start < prev {
954            return Err(RenderLosslessError::OverlappingSpans { start });
955        }
956        let gap = &source[prev..start];
957        if !gap.chars().all(char::is_whitespace) {
958            return Err(RenderLosslessError::UncoveredBytes {
959                start: prev,
960                end: start,
961                text: gap.to_string(),
962            });
963        }
964        out.push_str(gap);
965        out.push_str(&source[start..end]);
966        prev = end;
967    }
968    let tail = &source[prev..];
969    if !tail.chars().all(char::is_whitespace) {
970        return Err(RenderLosslessError::UncoveredTail {
971            start: prev,
972            text: tail.to_string(),
973        });
974    }
975    out.push_str(tail);
976    Ok(out)
977}
978
979/// A blank line is a whitespace-only line lying entirely between spans (so a
980/// blank-looking line inside a block comment or multiline string does not
981/// count). Emitted as one `BlankLines(n)` per maximal run so the printer can
982/// preserve paragraph breaks.
983fn add_blank_line_trivia(source: &str, tokens: &[Token], trivia: &mut Vec<Trivia>) {
984    let mut spans: Vec<(usize, usize)> = tokens
985        .iter()
986        .map(|t| (t.start, t.end))
987        .chain(trivia.iter().map(|t| (t.start, t.end)))
988        .collect();
989    spans.sort_unstable();
990    let bytes = source.as_bytes();
991    let mut blanks = Vec::new();
992    let mut gap_start = 0usize;
993    let emit_gap = |from: usize, to: usize, out: &mut Vec<Trivia>| {
994        // Newline offsets inside the gap. Full lines between two newlines
995        // (or before the first newline when the gap starts the file) are
996        // blank by construction: the gap holds no token/comment bytes, and
997        // any stray non-whitespace there fails the lossless render anyway.
998        let newlines: Vec<usize> = (from..to).filter(|&i| bytes[i] == b'\n').collect();
999        // Interior gap: the partial line before the first newline belongs to
1000        // the preceding token's line, so only lines between newlines count.
1001        // A gap at byte 0 has no such partial line — line 1 itself is blank.
1002        let count = if from == 0 {
1003            newlines.len()
1004        } else {
1005            newlines.len().saturating_sub(1)
1006        };
1007        if count == 0 {
1008            return;
1009        }
1010        let region_start = if from == 0 { 0 } else { newlines[0] + 1 };
1011        let region_end = newlines[newlines.len() - 1] + 1;
1012        let line = source[..region_start].matches('\n').count() + 1;
1013        out.push(Trivia {
1014            kind: TriviaKind::BlankLines(count),
1015            text: String::new(),
1016            pos: Pos { line, column: 1 },
1017            start: region_start,
1018            end: region_end,
1019        });
1020    };
1021    for &(s, e) in &spans {
1022        if s > gap_start {
1023            emit_gap(gap_start, s, &mut blanks);
1024        }
1025        gap_start = gap_start.max(e);
1026    }
1027    if source.len() > gap_start {
1028        emit_gap(gap_start, source.len(), &mut blanks);
1029    }
1030    trivia.extend(blanks);
1031}
1032
1033impl<'a> Lexer<'a> {
1034    fn peek(&self) -> Option<char> {
1035        self.chars.get(self.i).copied()
1036    }
1037
1038    fn peek_at(&self, n: usize) -> Option<char> {
1039        self.chars.get(self.i + n).copied()
1040    }
1041
1042    fn bump(&mut self) -> Option<char> {
1043        let c = self.chars.get(self.i).copied()?;
1044        self.i += 1;
1045        self.byte += c.len_utf8();
1046        match c {
1047            '\n' => {
1048                self.line += 1;
1049                self.column = 1;
1050            }
1051            '\t' => {
1052                // Tab advances to the next multiple-of-8 stop, matching GHC,
1053                // so mixed tabs/spaces don't silently corrupt layout.
1054                self.column = ((self.column - 1) / TAB_STOP + 1) * TAB_STOP + 1;
1055            }
1056            _ => self.column += 1,
1057        }
1058        Some(c)
1059    }
1060
1061    /// Next char when `peek` / `peek_at` already confirmed input remains.
1062    fn bump_after_peek(&mut self) -> char {
1063        self.bump()
1064            .expect("lexer cursor guarded by peek/peek_at before bump")
1065    }
1066
1067    const fn pos(&self) -> Pos {
1068        Pos {
1069            line: self.line,
1070            column: self.column,
1071        }
1072    }
1073
1074    /// `start` is the token's first byte; its end is wherever the cursor is
1075    /// now, so call this immediately after consuming the token.
1076    fn push(&mut self, tok: TokenKind, pos: Pos, start: usize) {
1077        self.tokens.push(Token {
1078            kind: tok,
1079            pos,
1080            start,
1081            end: self.byte,
1082        });
1083    }
1084
1085    fn push_trivia(&mut self, kind: TriviaKind, pos: Pos, start: usize) {
1086        if !self.capture_trivia {
1087            return;
1088        }
1089        self.trivia.push(Trivia {
1090            kind,
1091            text: self.src[start..self.byte].to_string(),
1092            pos,
1093            start,
1094            end: self.byte,
1095        });
1096    }
1097
1098    fn error(&mut self, kind: LexErrorKind, pos: Pos) {
1099        self.errors.push(LexError { kind, pos });
1100    }
1101
1102    fn scan_tokens(&mut self) {
1103        while let Some(c) = self.peek() {
1104            let pos = self.pos();
1105            let start = self.byte;
1106            match c {
1107                ' ' | '\t' | '\n' | '\r' => {
1108                    self.bump();
1109                }
1110                '(' => {
1111                    self.bump();
1112                    self.push(TokenKind::LParen, pos, start);
1113                }
1114                ')' => {
1115                    self.bump();
1116                    self.push(TokenKind::RParen, pos, start);
1117                }
1118                '[' => {
1119                    self.bump();
1120                    self.push(TokenKind::LBracket, pos, start);
1121                }
1122                ']' => {
1123                    self.bump();
1124                    self.push(TokenKind::RBracket, pos, start);
1125                }
1126                ',' => {
1127                    self.bump();
1128                    self.push(TokenKind::Comma, pos, start);
1129                }
1130                ';' => {
1131                    self.bump();
1132                    self.push(TokenKind::Semi, pos, start);
1133                }
1134                '`' => {
1135                    self.bump();
1136                    self.push(TokenKind::Backtick, pos, start);
1137                }
1138                '{' => {
1139                    if self.peek_at(1) == Some('-') {
1140                        self.block_comment(pos);
1141                    } else {
1142                        self.bump();
1143                        self.push(TokenKind::LBrace, pos, start);
1144                    }
1145                }
1146                '}' => {
1147                    self.bump();
1148                    self.push(TokenKind::RBrace, pos, start);
1149                }
1150                // CPP preprocessor directive (#ifdef/#endif/#include...) at
1151                // column 1 — daml-prim/stdlib sources use {-# LANGUAGE CPP #-};
1152                // directives are line-based, skip the whole line.
1153                '#' if self.column == 1
1154                    && self.peek_at(1).is_some_and(|c| c.is_ascii_lowercase()) =>
1155                {
1156                    while self.peek().is_some_and(|c| c != '\n') {
1157                        self.bump();
1158                    }
1159                    self.push_trivia(TriviaKind::CppDirective, pos, start);
1160                }
1161                '"' => self.string_lit(pos),
1162                '\'' => self.char_lit(pos),
1163                c if c.is_ascii_digit() => self.number(pos),
1164                c if is_ident_start(c) => self.identifier(pos),
1165                c if is_symbol_char(c) => self.operator(pos),
1166                _ => {
1167                    self.bump();
1168                    self.error(LexErrorKind::UnexpectedCharacter(c), pos);
1169                }
1170            }
1171        }
1172    }
1173
1174    /// `{- ... -}`, nested as in Haskell. Unterminated comment is an error
1175    /// but consumes to EOF (no hang, no panic).
1176    fn block_comment(&mut self, pos: Pos) {
1177        let start = self.byte;
1178        self.bump(); // {
1179        self.bump(); // -
1180        let mut depth = 1usize;
1181        while depth > 0 {
1182            match self.peek() {
1183                None => {
1184                    self.error(LexErrorKind::UnterminatedBlockComment, pos);
1185                    self.push_trivia(TriviaKind::BlockComment, pos, start);
1186                    return;
1187                }
1188                Some('{') if self.peek_at(1) == Some('-') => {
1189                    self.bump();
1190                    self.bump();
1191                    depth += 1;
1192                }
1193                Some('-') if self.peek_at(1) == Some('}') => {
1194                    self.bump();
1195                    self.bump();
1196                    depth -= 1;
1197                }
1198                Some(_) => {
1199                    self.bump();
1200                }
1201            }
1202        }
1203        self.push_trivia(TriviaKind::BlockComment, pos, start);
1204    }
1205
1206    fn string_lit(&mut self, pos: Pos) {
1207        let start = self.byte;
1208        self.bump(); // opening "
1209        let mut value = String::new();
1210        loop {
1211            match self.peek() {
1212                None | Some('\n') => {
1213                    self.error(LexErrorKind::UnterminatedStringLiteral, pos);
1214                    break;
1215                }
1216                Some('"') => {
1217                    self.bump();
1218                    break;
1219                }
1220                Some('\\') => {
1221                    let escape_pos = self.pos();
1222                    self.bump();
1223                    match self.peek() {
1224                        // String gap: backslash, whitespace, backslash.
1225                        Some(w) if w.is_whitespace() => {
1226                            while self.peek().is_some_and(|c| c.is_whitespace()) {
1227                                self.bump();
1228                            }
1229                            if self.peek() == Some('\\') {
1230                                self.bump();
1231                            } else {
1232                                self.error(LexErrorKind::UnterminatedStringGap, pos);
1233                                break;
1234                            }
1235                        }
1236                        Some(e) => {
1237                            self.bump();
1238                            match unescape(e) {
1239                                Some(c) => value.push(c),
1240                                None => {
1241                                    self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
1242                                    value.push(e);
1243                                }
1244                            }
1245                        }
1246                        None => {
1247                            self.error(LexErrorKind::UnterminatedStringLiteral, pos);
1248                            break;
1249                        }
1250                    }
1251                }
1252                Some(c) => {
1253                    self.bump();
1254                    value.push(c);
1255                }
1256            }
1257        }
1258        self.push(TokenKind::StringLit(value), pos, start);
1259    }
1260
1261    /// `'a'`, `'\n'`, `'\x41'`. A lone `'` that doesn't close within a few
1262    /// chars is not a char literal (identifiers consume their own primes, so
1263    /// this only triggers at expression positions).
1264    fn char_lit(&mut self, pos: Pos) {
1265        let start = self.byte;
1266        // Lookahead: find closing quote within a short window.
1267        let mut j = self.i + 1;
1268        let mut escaped = false;
1269        let mut ok = false;
1270        let window_end = (self.i + 12).min(self.chars.len());
1271        while j < window_end {
1272            match self.chars[j] {
1273                '\\' if !escaped => escaped = true,
1274                '\'' if !escaped => {
1275                    ok = j > self.i + 1;
1276                    break;
1277                }
1278                '\n' => break,
1279                _ => escaped = false,
1280            }
1281            j += 1;
1282        }
1283        if !ok {
1284            self.bump();
1285            self.error(LexErrorKind::StraySingleQuote, pos);
1286            return;
1287        }
1288        self.bump(); // opening '
1289        let mut value = String::new();
1290        while self.peek() != Some('\'') {
1291            let c = self.bump_after_peek();
1292            if c == '\\' {
1293                let escape_pos = Pos {
1294                    line: self.line,
1295                    column: self.column.saturating_sub(1),
1296                };
1297                if let Some(e) = self.bump() {
1298                    match unescape(e) {
1299                        Some(c) => value.push(c),
1300                        None => {
1301                            self.error(LexErrorKind::InvalidEscapeSequence(e), escape_pos);
1302                            value.push(e);
1303                        }
1304                    }
1305                }
1306            } else {
1307                value.push(c);
1308            }
1309        }
1310        self.bump(); // closing '
1311        if value.chars().count() != 1 {
1312            self.error(LexErrorKind::CharacterLiteralWrongLength, pos);
1313        }
1314        self.push(TokenKind::CharLit(value), pos, start);
1315    }
1316
1317    fn number(&mut self, pos: Pos) {
1318        let start = self.byte;
1319        let mut text = String::new();
1320        if self.peek() == Some('0') && matches!(self.peek_at(1), Some('x' | 'X')) {
1321            text.push(self.bump_after_peek());
1322            text.push(self.bump_after_peek());
1323            let mut has_hex_digit = false;
1324            while self
1325                .peek()
1326                .is_some_and(|c| c.is_ascii_hexdigit() || c == '_')
1327            {
1328                let c = self.bump_after_peek();
1329                has_hex_digit |= c.is_ascii_hexdigit();
1330                text.push(c);
1331            }
1332            if !has_hex_digit {
1333                self.error(LexErrorKind::HexLiteralMissingDigits, pos);
1334            }
1335            self.push(TokenKind::IntLit(text), pos, start);
1336            return;
1337        }
1338        while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
1339            text.push(self.bump_after_peek());
1340        }
1341        let mut decimal = false;
1342        // `1.5` is a decimal but `1..5` or `1.foo` is not.
1343        if self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) {
1344            decimal = true;
1345            text.push(self.bump_after_peek());
1346            while self.peek().is_some_and(|c| c.is_ascii_digit() || c == '_') {
1347                text.push(self.bump_after_peek());
1348            }
1349        }
1350        if matches!(self.peek(), Some('e' | 'E')) {
1351            decimal = true;
1352            if self.peek_at(1).is_some_and(|c| c.is_ascii_digit())
1353                || (matches!(self.peek_at(1), Some('+' | '-'))
1354                    && self.peek_at(2).is_some_and(|c| c.is_ascii_digit()))
1355            {
1356                text.push(self.bump_after_peek());
1357                if matches!(self.peek(), Some('+' | '-')) {
1358                    text.push(self.bump_after_peek());
1359                }
1360                while self.peek().is_some_and(|c| c.is_ascii_digit()) {
1361                    text.push(self.bump_after_peek());
1362                }
1363            } else {
1364                text.push(self.bump_after_peek());
1365                if matches!(self.peek(), Some('+' | '-')) {
1366                    text.push(self.bump_after_peek());
1367                }
1368                self.error(LexErrorKind::DecimalExponentMissingDigits, pos);
1369            }
1370        }
1371        if decimal {
1372            self.push(TokenKind::DecimalLit(text), pos, start);
1373        } else {
1374            self.push(TokenKind::IntLit(text), pos, start);
1375        }
1376    }
1377
1378    /// Identifiers, with greedy qualification: `DA.Set.fromList` is one
1379    /// token (qualifier "DA.Set", name "fromList").
1380    fn identifier(&mut self, pos: Pos) {
1381        let start = self.byte;
1382        let mut segments: Vec<String> = Vec::new();
1383        loop {
1384            let mut seg = String::new();
1385            while self.peek().is_some_and(is_ident_char) {
1386                seg.push(self.bump_after_peek());
1387            }
1388            let seg_is_upper = seg.chars().next().is_some_and(|c| c.is_uppercase());
1389            segments.push(seg);
1390            // Continue qualification only after an Upper segment: `Foo.bar`
1391            // is qualified, `foo.bar` is composition/projection.
1392            if seg_is_upper
1393                && self.peek() == Some('.')
1394                && self.peek_at(1).is_some_and(is_ident_start)
1395            {
1396                self.bump(); // .
1397                continue;
1398            }
1399            break;
1400        }
1401        let name = segments
1402            .pop()
1403            .expect("identifier loop always records at least one segment");
1404        let qualifier = if segments.is_empty() {
1405            None
1406        } else {
1407            Some(segments.join(".").into())
1408        };
1409        let tok = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
1410            TokenKind::UpperId {
1411                qualifier,
1412                name: name.into(),
1413            }
1414        } else {
1415            TokenKind::LowerId {
1416                qualifier,
1417                name: name.into(),
1418            }
1419        };
1420        self.push(tok, pos, start);
1421    }
1422
1423    fn operator(&mut self, pos: Pos) {
1424        let start = self.i;
1425        let byte_start = self.byte;
1426        while self.peek().is_some_and(is_symbol_char) {
1427            // `{-` inside an operator run can't happen ({ isn't a symbol
1428            // char), but `--` comment detection needs the full run first.
1429            self.bump();
1430        }
1431        let text: String = self.chars[start..self.i].iter().collect();
1432        // A run of 2+ dashes and nothing else is a line comment (Haskell
1433        // rule: `-->` is an operator, `--` and `---` start comments).
1434        if text.len() >= 2 && text.chars().all(|c| c == '-') {
1435            while self.peek().is_some_and(|c| c != '\n') {
1436                self.bump();
1437            }
1438            self.push_trivia(TriviaKind::LineComment, pos, byte_start);
1439            return;
1440        }
1441        self.push(TokenKind::Op(text.into()), pos, byte_start);
1442    }
1443}
1444
1445const fn unescape(c: char) -> Option<char> {
1446    match c {
1447        'n' => Some('\n'),
1448        't' => Some('\t'),
1449        'r' => Some('\r'),
1450        '0' => Some('\0'),
1451        'a' => Some('\u{07}'),
1452        'b' => Some('\u{08}'),
1453        'f' => Some('\u{0c}'),
1454        'v' => Some('\u{0b}'),
1455        '"' => Some('"'),
1456        '\'' => Some('\''),
1457        '\\' => Some('\\'),
1458        '&' => Some('&'),
1459        // DAML follows Haskell-style text escapes, including numeric escapes
1460        // (`\123`, `\o173`, `\x7B`) and named ASCII escapes (`\NUL`, `\SOH`,
1461        // ...). This lexer preserves source spans rather than fully decoding
1462        // multi-character escapes here, so accept the leading escape character
1463        // and let the remaining source characters flow through unchanged.
1464        '1'..='9' | 'o' | 'x' | 'A'..='Z' => Some(c),
1465        _ => None,
1466    }
1467}
1468
1469impl TokenKind {
1470    /// The identifier text if this is an unqualified lowercase identifier —
1471    /// how the parser checks for (contextual) keywords.
1472    #[must_use]
1473    pub const fn keyword(&self) -> Option<&str> {
1474        match self {
1475            Self::LowerId {
1476                qualifier: None,
1477                name,
1478            } => Some(name.as_str()),
1479            _ => None,
1480        }
1481    }
1482
1483    #[must_use]
1484    pub fn is_keyword(&self, kw: &str) -> bool {
1485        self.keyword() == Some(kw)
1486    }
1487
1488    #[must_use]
1489    pub fn is_op(&self, op: &str) -> bool {
1490        matches!(self, Self::Op(o) if o.as_str() == op)
1491    }
1492}
1493
1494// Tokenization, trivia, and lex/lex_with_trivia parity contracts for the lexer phase.
1495#[cfg(test)]
1496mod tests {
1497    use super::*;
1498
1499    fn toks(src: &str) -> Vec<TokenKind> {
1500        let (tokens, errors) = lex(src).into_parts();
1501        assert!(errors.is_empty(), "lex errors: {errors:?}");
1502        tokens.into_iter().map(|t| t.kind).collect()
1503    }
1504
1505    fn lex_error_messages(src: &str) -> Vec<String> {
1506        let (_, errors) = lex(src).into_parts();
1507        errors.into_iter().map(|e| e.to_string()).collect()
1508    }
1509
1510    fn lower(name: &str) -> TokenKind {
1511        TokenKind::LowerId {
1512            qualifier: None,
1513            name: name.into(),
1514        }
1515    }
1516
1517    fn upper(name: &str) -> TokenKind {
1518        TokenKind::UpperId {
1519            qualifier: None,
1520            name: name.into(),
1521        }
1522    }
1523
1524    #[test]
1525    fn identifier_as_ref_str() {
1526        let identifier = Identifier::from("value");
1527        let operator = Operator::from("+");
1528        let module = ModuleName::from("DA.Map");
1529
1530        assert_eq!(identifier.as_ref(), "value");
1531        assert_eq!(operator.as_ref(), "+");
1532        assert_eq!(module.as_ref(), "DA.Map");
1533    }
1534
1535    #[test]
1536    fn token_kind_display_is_normalized_not_lossless_source() {
1537        // Display is for diagnostics/logging. It intentionally does not
1538        // preserve whether punctuation came from source bytes or layout.
1539        assert_eq!(TokenKind::LBrace.to_string(), "{");
1540        assert_eq!(TokenKind::VLBrace.to_string(), "{");
1541        assert_eq!(TokenKind::Semi.to_string(), ";");
1542        assert_eq!(TokenKind::VSemi.to_string(), ";");
1543
1544        assert_eq!(
1545            TokenKind::UpperId {
1546                qualifier: Some("DA.Map".into()),
1547                name: "Map".into(),
1548            }
1549            .to_string(),
1550            "DA.Map.Map"
1551        );
1552        assert_eq!(TokenKind::Op("->".into()).to_string(), "->");
1553        assert_eq!(
1554            TokenKind::StringLit("line\n".into()).to_string(),
1555            "\"line\\n\""
1556        );
1557        assert_eq!(TokenKind::CharLit("'".into()).to_string(), "'\\''");
1558    }
1559
1560    #[test]
1561    fn line_comment_with_keywords_produces_no_tokens() {
1562        assert_eq!(toks("-- electing to exercise the option"), vec![]);
1563        assert_eq!(toks("--- template Foo"), vec![]);
1564    }
1565
1566    #[test]
1567    fn arrow_like_operator_is_not_comment() {
1568        assert_eq!(
1569            toks("a --> b"),
1570            vec![lower("a"), TokenKind::Op("-->".into()), lower("b")]
1571        );
1572    }
1573
1574    #[test]
1575    fn nested_block_comment() {
1576        assert_eq!(toks("{- outer {- inner -} still -} x"), vec![lower("x")]);
1577    }
1578
1579    #[test]
1580    fn string_with_keyword_and_escapes() {
1581        assert_eq!(
1582            toks(r#""template \"Foo\" \n""#),
1583            vec![TokenKind::StringLit("template \"Foo\" \n".into())]
1584        );
1585    }
1586
1587    #[test]
1588    fn qualified_identifiers() {
1589        assert_eq!(
1590            toks("DA.Set.fromList Map.Map foo"),
1591            vec![
1592                TokenKind::LowerId {
1593                    qualifier: Some("DA.Set".into()),
1594                    name: "fromList".into()
1595                },
1596                TokenKind::UpperId {
1597                    qualifier: Some("Map".into()),
1598                    name: "Map".into()
1599                },
1600                lower("foo"),
1601            ]
1602        );
1603    }
1604
1605    #[test]
1606    fn numbers() {
1607        assert_eq!(
1608            toks("42 1.5 0x1F 2e3 1_000"),
1609            vec![
1610                TokenKind::IntLit("42".into()),
1611                TokenKind::DecimalLit("1.5".into()),
1612                TokenKind::IntLit("0x1F".into()),
1613                TokenKind::DecimalLit("2e3".into()),
1614                TokenKind::IntLit("1_000".into()),
1615            ]
1616        );
1617    }
1618
1619    #[test]
1620    fn malformed_hex_literal_reports_error() {
1621        assert_eq!(
1622            lex_error_messages("0x 0x_"),
1623            vec![
1624                "hex literal requires at least one digit",
1625                "hex literal requires at least one digit",
1626            ]
1627        );
1628    }
1629
1630    #[test]
1631    fn malformed_decimal_exponent_reports_error() {
1632        assert_eq!(
1633            lex_error_messages("1e 1e+ 1e-"),
1634            vec![
1635                "decimal exponent requires at least one digit",
1636                "decimal exponent requires at least one digit",
1637                "decimal exponent requires at least one digit",
1638            ]
1639        );
1640    }
1641
1642    #[test]
1643    fn enum_from_to_is_not_decimal() {
1644        assert_eq!(
1645            toks("[1..5]"),
1646            vec![
1647                TokenKind::LBracket,
1648                TokenKind::IntLit("1".into()),
1649                TokenKind::Op("..".into()),
1650                TokenKind::IntLit("5".into()),
1651                TokenKind::RBracket,
1652            ]
1653        );
1654    }
1655
1656    #[test]
1657    fn primes_stay_in_identifier_and_char_lit_works() {
1658        assert_eq!(
1659            toks(r"foo' 'a' '\n'"),
1660            vec![
1661                lower("foo'"),
1662                TokenKind::CharLit("a".into()),
1663                TokenKind::CharLit("\n".into())
1664            ]
1665        );
1666    }
1667
1668    #[test]
1669    fn invalid_escape_sequences_report_errors() {
1670        assert_eq!(
1671            lex_error_messages(r#""\q" '\q'"#),
1672            vec!["invalid escape sequence \\q", "invalid escape sequence \\q"]
1673        );
1674    }
1675
1676    #[test]
1677    fn multi_character_char_literal_reports_error() {
1678        let (tokens, errors) = lex("'ab'").into_parts();
1679        assert_eq!(
1680            tokens.iter().map(|t| t.kind.clone()).collect::<Vec<_>>(),
1681            vec![TokenKind::CharLit("ab".into())]
1682        );
1683        assert_eq!(
1684            errors.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
1685            vec!["character literal must contain exactly one character".to_string()]
1686        );
1687    }
1688
1689    #[test]
1690    fn operators_and_punctuation() {
1691        assert_eq!(
1692            toks("x <- f (y, z) `div` 2"),
1693            vec![
1694                lower("x"),
1695                TokenKind::Op("<-".into()),
1696                lower("f"),
1697                TokenKind::LParen,
1698                lower("y"),
1699                TokenKind::Comma,
1700                lower("z"),
1701                TokenKind::RParen,
1702                TokenKind::Backtick,
1703                lower("div"),
1704                TokenKind::Backtick,
1705                TokenKind::IntLit("2".into()),
1706            ]
1707        );
1708    }
1709
1710    #[test]
1711    fn spans_are_one_based() {
1712        let (tokens, _) = lex("ab\n  cd").into_parts();
1713        assert_eq!(tokens[0].pos, Pos { line: 1, column: 1 });
1714        assert_eq!(tokens[1].pos, Pos { line: 2, column: 3 });
1715    }
1716
1717    #[test]
1718    fn tab_advances_to_stop() {
1719        let (tokens, _) = lex("\tx").into_parts();
1720        assert_eq!(tokens[0].pos, Pos { line: 1, column: 9 });
1721    }
1722
1723    #[test]
1724    fn unterminated_string_is_error_not_hang() {
1725        let (_, errors) = lex("x = \"oops\ny").into_parts();
1726        assert_eq!(errors.len(), 1);
1727    }
1728
1729    #[test]
1730    fn unterminated_block_comment_is_error_not_hang() {
1731        let (_, errors) = lex("{- never closed").into_parts();
1732        assert_eq!(errors.len(), 1);
1733    }
1734
1735    fn trivia_of(src: &str) -> Vec<Trivia> {
1736        let (_, trivia, _) = lex_with_trivia(src).into_parts();
1737        trivia
1738    }
1739
1740    /// The lossless oracle on one source: spans must tile the file and the
1741    /// reconstruction must be byte-identical.
1742    fn assert_round_trip(src: &str) {
1743        let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1744        assert!(errors.is_empty(), "lex errors: {errors:?}");
1745        assert_eq!(
1746            render_lossless(src, &tokens, &trivia).as_deref(),
1747            Ok(src),
1748            "round trip failed for {src:?}"
1749        );
1750    }
1751
1752    #[test]
1753    fn line_comment_becomes_trivia_with_exact_text_and_span() {
1754        let src = "x = 1 -- electing to exercise\ny = 2\n";
1755        let trivia = trivia_of(src);
1756        assert_eq!(trivia.len(), 1);
1757        assert_eq!(trivia[0].kind, TriviaKind::LineComment);
1758        assert_eq!(trivia[0].text, "-- electing to exercise");
1759        assert_eq!(&src[trivia[0].start..trivia[0].end], trivia[0].text);
1760        assert_eq!(trivia[0].pos, Pos { line: 1, column: 7 });
1761    }
1762
1763    #[test]
1764    fn nested_block_comment_becomes_one_trivia() {
1765        let src = "{- outer {- inner -} still -} x";
1766        let trivia = trivia_of(src);
1767        assert_eq!(trivia.len(), 1);
1768        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1769        assert_eq!(trivia[0].text, "{- outer {- inner -} still -}");
1770    }
1771
1772    #[test]
1773    fn unterminated_block_comment_still_yields_trivia_to_eof() {
1774        let (_, trivia, errors) = lex_with_trivia("x {- never closed").into_parts();
1775        assert_eq!(errors.len(), 1);
1776        assert_eq!(trivia.len(), 1);
1777        assert_eq!(trivia[0].text, "{- never closed");
1778    }
1779
1780    #[test]
1781    fn blank_lines_between_items_counted() {
1782        let src = "x = 1\n\n\ny = 2\n";
1783        let trivia = trivia_of(src);
1784        assert_eq!(trivia.len(), 1);
1785        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(2));
1786        assert_eq!(trivia[0].pos, Pos { line: 2, column: 1 });
1787    }
1788
1789    #[test]
1790    fn blank_line_at_file_start_counted() {
1791        let trivia = trivia_of("\nx = 1\n");
1792        assert_eq!(trivia.len(), 1);
1793        assert_eq!(trivia[0].kind, TriviaKind::BlankLines(1));
1794        assert_eq!(trivia[0].pos, Pos { line: 1, column: 1 });
1795    }
1796
1797    #[test]
1798    fn blank_looking_lines_inside_block_comment_are_not_blank_trivia() {
1799        let src = "x = 1 {- a\n\nb -}\ny = 2\n";
1800        let trivia = trivia_of(src);
1801        assert_eq!(trivia.len(), 1, "{trivia:?}");
1802        assert_eq!(trivia[0].kind, TriviaKind::BlockComment);
1803    }
1804
1805    #[test]
1806    fn blank_line_between_comments_counted() {
1807        let src = "-- a\n\n-- b\nx = 1\n";
1808        let kinds: Vec<_> = trivia_of(src).into_iter().map(|t| t.kind).collect();
1809        assert_eq!(
1810            kinds,
1811            vec![
1812                TriviaKind::LineComment,
1813                TriviaKind::BlankLines(1),
1814                TriviaKind::LineComment,
1815            ]
1816        );
1817    }
1818
1819    #[test]
1820    fn cpp_directive_becomes_trivia() {
1821        let src = "#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n";
1822        let trivia = trivia_of(src);
1823        assert_eq!(trivia.len(), 2);
1824        assert!(trivia.iter().all(|t| t.kind == TriviaKind::CppDirective));
1825        assert_eq!(trivia[0].text, "#ifdef DAML_BIGNUMERIC");
1826    }
1827
1828    #[test]
1829    fn round_trip_is_byte_identical() {
1830        assert_round_trip("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1831        assert_round_trip("x = \"tem\\\"plate \\n\" {- block {- nested -} -}\r\ny = 'a'\r\n");
1832        assert_round_trip("\tärger = [1..5] -- ütf\n");
1833        assert_round_trip("s = \"gap \\  \\ here\"\n");
1834        assert_round_trip("#ifdef X\nf = 0x1F\n#endif\n");
1835        assert_round_trip("\n\n  \nf = 1  \n   ");
1836        assert_round_trip("");
1837    }
1838
1839    #[test]
1840    fn render_lossless_detects_lost_bytes() {
1841        let src = "x = 1 -- comment\n";
1842        let (tokens, mut trivia, _) = lex_with_trivia(src).into_parts();
1843        trivia.clear(); // simulate a lexer that drops the comment
1844        assert!(render_lossless(src, &tokens, &trivia).is_err());
1845    }
1846
1847    #[test]
1848    fn unicode_identifier() {
1849        assert_eq!(
1850            toks("ärger = 1"),
1851            vec![
1852                lower("ärger"),
1853                TokenKind::Op("=".into()),
1854                TokenKind::IntLit("1".into())
1855            ]
1856        );
1857        let _ = upper("Ülf"); // helper used
1858    }
1859
1860    /// Parse-only `lex` must emit the same tokens as `lex_with_trivia`; only
1861    /// trivia differs. Comments and blank lines must not change tokenization.
1862    fn assert_lex_tokens_match(src: &str) {
1863        let (parse_tokens, parse_errors) = lex(src).into_parts();
1864        let (trivia_tokens, _, trivia_errors) = lex_with_trivia(src).into_parts();
1865        assert_eq!(parse_errors, trivia_errors, "lex errors differ for {src:?}");
1866        assert_eq!(
1867            parse_tokens.len(),
1868            trivia_tokens.len(),
1869            "token count for {src:?}"
1870        );
1871        for (a, b) in parse_tokens.iter().zip(trivia_tokens.iter()) {
1872            assert_eq!(a.kind, b.kind, "token kind for {src:?}");
1873            assert_eq!(a.pos, b.pos, "token pos for {src:?}");
1874            assert_eq!(a.start, b.start, "token start for {src:?}");
1875            assert_eq!(a.end, b.end, "token end for {src:?}");
1876        }
1877    }
1878
1879    #[test]
1880    fn lex_and_lex_with_trivia_emit_identical_tokens() {
1881        assert_lex_tokens_match("x = 1 -- electing to exercise\ny = 2\n");
1882        assert_lex_tokens_match("module M where\n\n-- doc\nf : Int -> Int\nf x = x + 1\n");
1883        assert_lex_tokens_match("{- outer {- inner -} still -} x");
1884        assert_lex_tokens_match("#ifdef DAML_BIGNUMERIC\nx = 1\n#endif\n");
1885        assert_lex_tokens_match("\n\n  \nf = 1  \n   ");
1886    }
1887
1888    #[test]
1889    fn lex_with_trivia_preserves_lossless_comment_and_blank_line_render() {
1890        let sources = [
1891            "x = 1 -- electing to exercise\ny = 2\n",
1892            "x = 1\n\n\ny = 2\n",
1893            "-- a\n\n-- b\nx = 1\n",
1894        ];
1895        for src in sources {
1896            let (tokens, trivia, errors) = lex_with_trivia(src).into_parts();
1897            assert!(errors.is_empty(), "lex errors for {src:?}: {errors:?}");
1898            assert_eq!(
1899                render_lossless(src, &tokens, &trivia).as_deref(),
1900                Ok(src),
1901                "lossless render failed for {src:?}"
1902            );
1903        }
1904    }
1905}