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