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