Skip to main content

daml_syntax/
lib.rs

1//! Shared parsed-source surface for Daml tools.
2//!
3//! `daml-parser` stays the low-level lexer/layout/parser implementation.
4//! This crate owns the source-facing facts tools need around that parser:
5//! diagnostics, line/UTF-16 mapping, tokens, trivia, laid-out tokens, and
6//! conversion from parser byte spans to `text-size` ranges.
7//!
8//! ```rust
9//! use daml_syntax::{parser_span_to_text_range, SourceFile};
10//!
11//! let source = "module M where\nfoo : Int\nfoo = 1\n";
12//! let file = SourceFile::parse(source);
13//!
14//! assert_eq!(file.module().name, "M");
15//! assert!(file.diagnostics().is_empty());
16//! assert!(!file.tokens().is_empty());
17//! assert!(!file.laid_out_tokens().is_empty());
18//!
19//! let header_range = parser_span_to_text_range(source, file.module().header);
20//! assert_eq!(usize::from(header_range.start()), 0);
21//! assert_eq!(header_range, file.parser_span_to_text_range(file.module().header));
22//! ```
23
24use daml_parser::ast::{DiagnosticCategory, Module, Span as ParserSpan};
25use daml_parser::layout::resolve_layout;
26use daml_parser::lexer::{lex_with_trivia, LexError, Token, Trivia};
27use daml_parser::parse::parse_module;
28use std::sync::OnceLock;
29
30pub mod coordinate;
31
32pub use coordinate::{
33    ByteColumn, ByteLineCol, ByteOffset, CharColumn, CharLineCol, Coordinate, LineNumber,
34    Utf16Offset,
35};
36pub use text_size::{TextRange, TextSize};
37
38/// A parser or lexer diagnostic anchored in source text.
39///
40/// Constructed by [`SourceFile::parse`]; read fields through accessors so future
41/// metadata (severity, codes, notes) can be added without breaking callers.
42#[derive(Debug, Clone, PartialEq, Eq)]
43#[non_exhaustive]
44pub struct Diagnostic {
45    range: TextRange,
46    line: LineNumber,
47    column: CharColumn,
48    end_column: Option<CharColumn>,
49    message: String,
50    category: DiagnosticCategory,
51}
52
53impl Diagnostic {
54    #[must_use]
55    pub const fn range(&self) -> TextRange {
56        self.range
57    }
58
59    #[must_use]
60    pub const fn line(&self) -> LineNumber {
61        self.line
62    }
63
64    #[must_use]
65    pub const fn column(&self) -> CharColumn {
66        self.column
67    }
68
69    #[must_use]
70    pub const fn end_column(&self) -> Option<CharColumn> {
71        self.end_column
72    }
73
74    #[must_use]
75    pub fn message(&self) -> &str {
76        &self.message
77    }
78
79    #[must_use]
80    pub const fn category(&self) -> DiagnosticCategory {
81        self.category
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct LineIndex {
87    source_len: usize,
88    line_start_bytes: Vec<usize>,
89    char_offset_by_byte: Vec<usize>,
90    utf16_offset_by_byte: Vec<usize>,
91}
92
93impl LineIndex {
94    #[must_use]
95    pub fn new(source: &str) -> Self {
96        let mut line_start_bytes = vec![0];
97        for (idx, byte) in source.bytes().enumerate() {
98            if byte == b'\n' {
99                line_start_bytes.push(idx + 1);
100            }
101        }
102
103        let mut char_offset_by_byte = vec![0; source.len() + 1];
104        let mut char_count = 0usize;
105        let mut prev = 0usize;
106        for (idx, ch) in source.char_indices() {
107            for slot in char_offset_by_byte.iter_mut().take(idx).skip(prev) {
108                *slot = char_count;
109            }
110            let char_end = idx + ch.len_utf8();
111            for slot in char_offset_by_byte.iter_mut().take(char_end).skip(idx) {
112                *slot = char_count;
113            }
114            char_count += 1;
115            prev = char_end;
116        }
117        for slot in char_offset_by_byte
118            .iter_mut()
119            .take(source.len() + 1)
120            .skip(prev)
121        {
122            *slot = char_count;
123        }
124
125        let mut utf16_offset_by_byte = vec![0; source.len() + 1];
126        let mut utf16 = 0usize;
127        let mut prev = 0usize;
128        for (idx, ch) in source.char_indices() {
129            for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
130                *slot = utf16;
131            }
132            let char_end = idx + ch.len_utf8();
133            for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
134                *slot = utf16;
135            }
136            utf16 += ch.len_utf16();
137            prev = char_end;
138        }
139        for slot in utf16_offset_by_byte
140            .iter_mut()
141            .take(source.len() + 1)
142            .skip(prev)
143        {
144            *slot = utf16;
145        }
146
147        Self {
148            source_len: source.len(),
149            line_start_bytes,
150            char_offset_by_byte,
151            utf16_offset_by_byte,
152        }
153    }
154
155    #[must_use]
156    pub fn line_col(&self, offset: TextSize) -> ByteLineCol {
157        let byte = usize::from(offset).min(self.source_len);
158        let line_idx = match self.line_start_bytes.binary_search(&byte) {
159            Ok(idx) => idx,
160            Err(idx) => idx.saturating_sub(1),
161        };
162        ByteLineCol {
163            line: LineNumber::new(line_idx + 1),
164            column: ByteColumn::new(byte - self.line_start_bytes[line_idx] + 1),
165        }
166    }
167
168    #[must_use]
169    pub fn char_line_col(&self, offset: TextSize) -> CharLineCol {
170        let byte = usize::from(offset).min(self.source_len);
171        let line_idx = match self.line_start_bytes.binary_search(&byte) {
172            Ok(idx) => idx,
173            Err(idx) => idx.saturating_sub(1),
174        };
175        let line_start = self.line_start_bytes[line_idx];
176        CharLineCol {
177            line: LineNumber::new(line_idx + 1),
178            column: CharColumn::new(
179                self.char_offset_by_byte[byte] - self.char_offset_by_byte[line_start] + 1,
180            ),
181        }
182    }
183
184    #[must_use]
185    pub fn utf16_col(&self, line: LineNumber, byte_col: ByteColumn) -> Utf16Offset {
186        let line_start = self
187            .line_start_bytes
188            .get(line.get().saturating_sub(1))
189            .copied()
190            .unwrap_or(self.source_len);
191        let byte = line_start
192            .saturating_add(byte_col.get().saturating_sub(1))
193            .min(self.source_len);
194        Utf16Offset::new(self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start])
195    }
196
197    #[must_use]
198    pub fn utf16_range(&self, range: TextRange) -> (Utf16Offset, Utf16Offset) {
199        let start = usize::from(range.start()).min(self.source_len);
200        let end = usize::from(range.end()).min(self.source_len).max(start);
201        (
202            Utf16Offset::new(self.utf16_offset_by_byte[start]),
203            Utf16Offset::new(self.utf16_offset_by_byte[end]),
204        )
205    }
206}
207
208#[derive(Debug)]
209pub struct SourceTokens {
210    tokens: Vec<Token>,
211    trivia: Vec<Trivia>,
212    lex_errors: Vec<LexError>,
213    laid_out_tokens: OnceLock<Vec<Token>>,
214}
215
216impl SourceTokens {
217    #[must_use]
218    pub fn lex(source: &str) -> Self {
219        let lexed = lex_with_trivia(source);
220        Self {
221            tokens: lexed.tokens,
222            trivia: lexed.trivia,
223            lex_errors: lexed.errors,
224            laid_out_tokens: OnceLock::new(),
225        }
226    }
227
228    #[must_use]
229    pub fn tokens(&self) -> &[Token] {
230        &self.tokens
231    }
232
233    #[must_use]
234    pub fn trivia(&self) -> &[Trivia] {
235        &self.trivia
236    }
237
238    #[must_use]
239    pub fn lex_errors(&self) -> &[LexError] {
240        &self.lex_errors
241    }
242
243    #[must_use]
244    pub fn laid_out_tokens(&self) -> &[Token] {
245        self.laid_out_tokens
246            .get_or_init(|| resolve_layout(self.tokens.as_slice()))
247    }
248}
249
250#[derive(Debug)]
251pub struct SourceFile {
252    source: String,
253    module: Module,
254    diagnostics: Vec<Diagnostic>,
255    line_index: LineIndex,
256    tokens: OnceLock<SourceTokens>,
257}
258
259impl SourceFile {
260    #[must_use]
261    pub fn parse(source: &str) -> Self {
262        let parsed = parse_module(source);
263        let line_index = LineIndex::new(source);
264        let diagnostics = parsed
265            .diagnostics
266            .into_iter()
267            .map(|diagnostic| {
268                let range = try_parser_span_to_text_range(source, diagnostic.span)
269                    .expect("parser span in diagnostic must map to source bytes");
270                let start = range.start();
271                let end_column = source
272                    .get(usize::from(range.start())..usize::from(range.end()))
273                    .filter(|s| !s.is_empty() && !s.contains('\n'))
274                    .map(|s| CharColumn::new(diagnostic.pos.column + s.chars().count()));
275                let start_pos = line_index.char_line_col(start);
276                Diagnostic {
277                    range,
278                    line: start_pos.line,
279                    column: CharColumn::new(diagnostic.pos.column),
280                    end_column,
281                    message: diagnostic.message,
282                    category: diagnostic.category,
283                }
284            })
285            .collect();
286
287        Self {
288            source: source.to_string(),
289            module: parsed.module,
290            diagnostics,
291            line_index,
292            tokens: OnceLock::new(),
293        }
294    }
295
296    #[must_use]
297    pub fn source(&self) -> &str {
298        &self.source
299    }
300
301    #[must_use]
302    pub const fn module(&self) -> &Module {
303        &self.module
304    }
305
306    #[must_use]
307    pub fn diagnostics(&self) -> &[Diagnostic] {
308        &self.diagnostics
309    }
310
311    #[must_use]
312    pub const fn line_index(&self) -> &LineIndex {
313        &self.line_index
314    }
315
316    #[must_use]
317    pub fn tokens(&self) -> &[Token] {
318        self.source_tokens().tokens()
319    }
320
321    #[must_use]
322    pub fn trivia(&self) -> &[Trivia] {
323        self.source_tokens().trivia()
324    }
325
326    #[must_use]
327    pub fn laid_out_tokens(&self) -> &[Token] {
328        self.source_tokens().laid_out_tokens()
329    }
330
331    /// Convert a parser span from this source into a `text-size` byte range.
332    ///
333    /// This is the convenience API for spans that originate from this source file
334    /// and are expected to be valid.
335    ///
336    /// # Panics
337    ///
338    /// Panics when `span` does not map to valid UTF-8 source bytes in this
339    /// source.
340    #[must_use]
341    pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
342        self.try_parser_span_to_text_range(span)
343            .expect("parser span must map to a valid UTF-8 range in source")
344    }
345
346    /// Try to convert a parser span from this source into a `text-size` byte
347    /// range.
348    ///
349    /// This fallible API is the preferred choice for spans from external or
350    /// untrusted sources where offsets may be invalid. Use
351    /// [`SourceFile::parser_span_to_text_range`] for spans that originate from
352    /// this source and are expected to map to valid UTF-8 bytes.
353    #[must_use = "handle invalid span offsets before using the range"]
354    pub fn try_parser_span_to_text_range(
355        &self,
356        span: ParserSpan,
357    ) -> Result<TextRange, ParserSpanToTextRangeError> {
358        try_parser_span_to_text_range(&self.source, span)
359    }
360
361    fn source_tokens(&self) -> &SourceTokens {
362        self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
363    }
364}
365
366/// Convert a parser span into a `text-size` byte range for an arbitrary source
367/// string.
368///
369/// This is a convenience wrapper around [`try_parser_span_to_text_range`] for
370/// spans that are expected to be valid.
371///
372/// # Panics
373///
374/// Panics when `span` does not map to valid UTF-8 source bytes in `source`.
375#[must_use]
376pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
377    try_parser_span_to_text_range(source, span)
378        .expect("parser span must map to a valid UTF-8 range")
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382#[non_exhaustive]
383pub enum ParserSpanToTextRangeErrorKind {
384    OutOfBounds,
385    InvertedSpan,
386    NonUtf8Boundary,
387    TextSizeOverflow,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub struct ParserSpanToTextRangeError {
392    source_len: usize,
393    span_start: usize,
394    span_end: usize,
395    kind: ParserSpanToTextRangeErrorKind,
396}
397
398impl ParserSpanToTextRangeError {
399    #[must_use]
400    pub const fn source_len(&self) -> usize {
401        self.source_len
402    }
403
404    #[must_use]
405    pub const fn span_start(&self) -> usize {
406        self.span_start
407    }
408
409    #[must_use]
410    pub const fn span_end(&self) -> usize {
411        self.span_end
412    }
413
414    #[must_use]
415    pub const fn kind(&self) -> ParserSpanToTextRangeErrorKind {
416        self.kind
417    }
418}
419
420impl std::fmt::Display for ParserSpanToTextRangeError {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        match self.kind {
423            ParserSpanToTextRangeErrorKind::TextSizeOverflow => write!(
424                f,
425                "parser span [{}, {}) cannot be represented as a text-size value",
426                self.span_start, self.span_end
427            ),
428            _ => write!(
429                f,
430                "parser span [{}, {}) is invalid for source length {}",
431                self.span_start, self.span_end, self.source_len
432            ),
433        }
434    }
435}
436
437impl std::error::Error for ParserSpanToTextRangeError {}
438
439/// Try to convert a parser span into a `text-size` byte range.
440///
441/// This is the fallible API and should be used for spans sourced outside
442/// `SourceFile` where invalid offsets are possible; offsets must be valid
443/// UTF-8 character boundaries.
444#[must_use = "handle invalid span offsets before converting"]
445pub fn try_parser_span_to_text_range(
446    source: &str,
447    span: ParserSpan,
448) -> Result<TextRange, ParserSpanToTextRangeError> {
449    let source_len = source.len();
450    if span.start > source_len || span.end > source_len {
451        return Err(ParserSpanToTextRangeError {
452            source_len,
453            span_start: span.start,
454            span_end: span.end,
455            kind: ParserSpanToTextRangeErrorKind::OutOfBounds,
456        });
457    }
458    if span.start > span.end {
459        return Err(ParserSpanToTextRangeError {
460            source_len,
461            span_start: span.start,
462            span_end: span.end,
463            kind: ParserSpanToTextRangeErrorKind::InvertedSpan,
464        });
465    }
466    if !source.is_char_boundary(span.start) || !source.is_char_boundary(span.end) {
467        return Err(ParserSpanToTextRangeError {
468            source_len,
469            span_start: span.start,
470            span_end: span.end,
471            kind: ParserSpanToTextRangeErrorKind::NonUtf8Boundary,
472        });
473    }
474    Ok(TextRange::new(
475        TextSize::try_from(span.start).map_err(|_| ParserSpanToTextRangeError {
476            source_len,
477            span_start: span.start,
478            span_end: span.end,
479            kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
480        })?,
481        TextSize::try_from(span.end).map_err(|_| ParserSpanToTextRangeError {
482            source_len,
483            span_start: span.start,
484            span_end: span.end,
485            kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
486        })?,
487    ))
488}
489
490#[cfg(test)]
491#[allow(clippy::unwrap_used)]
492mod tests {
493    use super::*;
494    use daml_parser::ast_span::render_from_ast;
495    use daml_parser::lexer::render_lossless;
496
497    #[test]
498    fn maps_empty_source_to_first_line() {
499        let index = LineIndex::new("");
500
501        assert_eq!(
502            index.line_col(0.into()),
503            ByteLineCol {
504                line: LineNumber::new(1),
505                column: ByteColumn::new(1),
506            }
507        );
508        assert_eq!(
509            index.utf16_range(TextRange::empty(0.into())),
510            (Utf16Offset::new(0), Utf16Offset::new(0))
511        );
512    }
513
514    #[test]
515    fn maps_ascii_byte_lines() {
516        let source = "module M where\nfoo = 1\n";
517        let index = LineIndex::new(source);
518
519        assert_eq!(
520            index.line_col(15.into()),
521            ByteLineCol {
522                line: LineNumber::new(2),
523                column: ByteColumn::new(1),
524            }
525        );
526        assert_eq!(
527            index.utf16_col(LineNumber::new(2), ByteColumn::new(4)),
528            Utf16Offset::new(3)
529        );
530    }
531
532    #[test]
533    fn maps_utf8_and_utf16_offsets() {
534        let source = "a😀b\nz";
535        let index = LineIndex::new(source);
536
537        assert_eq!(
538            index.utf16_range(TextRange::new(0.into(), 6.into())),
539            (Utf16Offset::new(0), Utf16Offset::new(4))
540        );
541        assert_eq!(
542            index.utf16_col(LineNumber::new(1), ByteColumn::new(6)),
543            Utf16Offset::new(3)
544        );
545        assert_eq!(
546            index.char_line_col(5.into()),
547            CharLineCol {
548                line: LineNumber::new(1),
549                column: CharColumn::new(3),
550            }
551        );
552    }
553
554    #[test]
555    fn char_line_col_snaps_to_previous_utf8_boundary() {
556        let source = "a😀b";
557        let index = LineIndex::new(source);
558
559        // Offset 3 is inside the 4-byte 😀 sequence (1..5), so we expect snapping to 1.
560        assert_eq!(
561            index.char_line_col(3.into()),
562            CharLineCol {
563                line: LineNumber::new(1),
564                column: CharColumn::new(2),
565            }
566        );
567    }
568
569    #[test]
570    fn preserves_trailing_newline_line_start() {
571        let index = LineIndex::new("a\n");
572
573        assert_eq!(
574            index.line_col(2.into()),
575            ByteLineCol {
576                line: LineNumber::new(2),
577                column: ByteColumn::new(1),
578            }
579        );
580    }
581
582    #[test]
583    fn treats_crlf_as_bytes_without_normalization() {
584        let index = LineIndex::new("a\r\nb");
585
586        assert_eq!(
587            index.line_col(3.into()),
588            ByteLineCol {
589                line: LineNumber::new(2),
590                column: ByteColumn::new(1),
591            }
592        );
593    }
594
595    #[test]
596    fn clamps_ranges_to_source_end() {
597        let index = LineIndex::new("abc");
598        let range = TextRange::new(1.into(), 99.into());
599
600        assert_eq!(
601            index.utf16_range(range),
602            (Utf16Offset::new(1), Utf16Offset::new(3))
603        );
604    }
605
606    #[test]
607    fn byte_and_char_line_col_columns_differ_for_multibyte_utf8() {
608        let source = "😀\n";
609        let index = LineIndex::new(source);
610        let offset = TextSize::from(4);
611
612        let byte_pos = index.line_col(offset);
613        let char_pos = index.char_line_col(offset);
614
615        assert_eq!(byte_pos.line, char_pos.line);
616        assert_ne!(byte_pos.column.get(), char_pos.column.get());
617        assert_eq!(byte_pos.column, ByteColumn::new(5));
618        assert_eq!(char_pos.column, CharColumn::new(2));
619    }
620
621    #[test]
622    fn source_file_exposes_parser_pipeline_facts() {
623        let source = "module M where\nfoo : Int\nfoo = 1\n";
624        let file = SourceFile::parse(source);
625
626        assert_eq!(file.source(), source);
627        assert_eq!(file.module().name, "M");
628        assert!(file.diagnostics().is_empty());
629        assert!(!file.tokens().is_empty());
630        assert!(!file.laid_out_tokens().is_empty());
631        assert_eq!(
632            render_lossless(source, file.tokens(), file.trivia()).as_deref(),
633            Ok(source)
634        );
635        assert_eq!(
636            render_from_ast(source, file.module(), file.trivia()).as_deref(),
637            Ok(source)
638        );
639    }
640
641    #[test]
642    fn source_tokens_exposes_lex_only_pipeline_facts() {
643        let source = "module M where\nfoo : Int\nfoo = 1\n";
644        let tokens = SourceTokens::lex(source);
645
646        assert!(tokens.lex_errors().is_empty());
647        assert!(!tokens.tokens().is_empty());
648        assert!(!tokens.laid_out_tokens().is_empty());
649        assert_eq!(
650            render_lossless(source, tokens.tokens(), tokens.trivia()).as_deref(),
651            Ok(source)
652        );
653    }
654
655    #[test]
656    fn malformed_source_keeps_source_file_and_diagnostics() {
657        let file = SourceFile::parse("module M where\nfoo = \"unterminated\nbar = 1\n");
658
659        assert_eq!(file.module().name, "M");
660        assert!(file
661            .diagnostics()
662            .iter()
663            .any(|diagnostic| diagnostic.category() == DiagnosticCategory::Lex));
664    }
665
666    #[test]
667    fn converts_parser_spans_to_text_ranges() {
668        let file = SourceFile::parse("module M where\nfoo = 1\n");
669        let source_len = file.source().len();
670        let range = file.parser_span_to_text_range(ParserSpan::new(0, source_len));
671
672        assert_eq!(
673            range,
674            TextRange::new(0.into(), source_len.try_into().unwrap())
675        );
676    }
677
678    #[test]
679    fn try_parser_span_to_text_range_rejects_out_of_bounds_spans() {
680        let source = "module M where\nfoo = 1\n";
681        let err = try_parser_span_to_text_range(source, ParserSpan::new(0, source.len() + 1))
682            .unwrap_err();
683        assert_eq!(err.kind(), ParserSpanToTextRangeErrorKind::OutOfBounds);
684        assert_eq!(
685            err.to_string(),
686            format!(
687                "parser span [0, {}) is invalid for source length {}",
688                source.len() + 1,
689                source.len()
690            )
691        );
692        assert_eq!(err.source_len(), source.len());
693        assert_eq!(err.span_start(), 0);
694        assert_eq!(err.span_end(), source.len() + 1);
695    }
696
697    #[test]
698    fn try_parser_span_to_text_range_reports_inverted_spans() {
699        let source = "abc";
700        let err = try_parser_span_to_text_range(source, ParserSpan::new(2, 1)).unwrap_err();
701        assert_eq!(err.kind(), ParserSpanToTextRangeErrorKind::InvertedSpan);
702        assert_eq!(
703            err.to_string(),
704            "parser span [2, 1) is invalid for source length 3"
705        );
706        assert_eq!(err.source_len(), source.len());
707        assert_eq!(err.span_start(), 2);
708        assert_eq!(err.span_end(), 1);
709    }
710
711    #[test]
712    fn try_parser_span_to_text_range_rejects_non_utf8_boundary_spans() {
713        let source = "a😀b";
714        let err = try_parser_span_to_text_range(source, ParserSpan::new(1, 2)).unwrap_err();
715        assert_eq!(err.kind(), ParserSpanToTextRangeErrorKind::NonUtf8Boundary);
716
717        assert_eq!(
718            err.to_string(),
719            "parser span [1, 2) is invalid for source length 6"
720        );
721        assert_eq!(err.source_len(), source.len());
722        assert_eq!(err.span_start(), 1);
723        assert_eq!(err.span_end(), 2);
724    }
725
726    #[test]
727    fn diagnostics_are_read_through_accessors_not_field_literals() {
728        let file = SourceFile::parse("module M where\nfoo = \"unterminated\n");
729
730        let diagnostic = file
731            .diagnostics()
732            .first()
733            .expect("malformed source should surface diagnostics");
734        assert_eq!(diagnostic.category(), DiagnosticCategory::Lex);
735        assert!(!diagnostic.message().is_empty());
736        assert!(diagnostic.range().start() <= diagnostic.range().end());
737        assert!(diagnostic.line().get() >= 1);
738        assert!(diagnostic.column().get() >= 1);
739    }
740
741    #[test]
742    fn try_parser_span_to_text_range_succeeds_for_valid_span() {
743        let source = "module M where\nfoo = 1\n";
744        let range = try_parser_span_to_text_range(source, ParserSpan::new(0, 5))
745            .expect("span should be valid");
746        assert_eq!(range, TextRange::new(0.into(), 5.into()));
747    }
748}