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    utf16_offset_by_byte: Vec<usize>,
53}
54
55impl LineIndex {
56    #[must_use]
57    pub fn new(source: &str) -> Self {
58        let mut line_start_bytes = vec![0];
59        for (idx, byte) in source.bytes().enumerate() {
60            if byte == b'\n' {
61                line_start_bytes.push(idx + 1);
62            }
63        }
64
65        let mut utf16_offset_by_byte = vec![0; source.len() + 1];
66        let mut utf16 = 0usize;
67        let mut prev = 0usize;
68        for (idx, ch) in source.char_indices() {
69            for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
70                *slot = utf16;
71            }
72            let char_end = idx + ch.len_utf8();
73            for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
74                *slot = utf16;
75            }
76            utf16 += ch.len_utf16();
77            prev = char_end;
78        }
79        for slot in utf16_offset_by_byte
80            .iter_mut()
81            .take(source.len() + 1)
82            .skip(prev)
83        {
84            *slot = utf16;
85        }
86
87        Self {
88            source_len: source.len(),
89            line_start_bytes,
90            utf16_offset_by_byte,
91        }
92    }
93
94    #[must_use]
95    pub fn line_col(&self, offset: TextSize) -> LineCol {
96        let byte = usize::from(offset).min(self.source_len);
97        let line_idx = match self.line_start_bytes.binary_search(&byte) {
98            Ok(idx) => idx,
99            Err(idx) => idx.saturating_sub(1),
100        };
101        LineCol {
102            line: line_idx + 1,
103            column: byte - self.line_start_bytes[line_idx] + 1,
104        }
105    }
106
107    #[must_use]
108    pub fn char_line_col(&self, source: &str, offset: TextSize) -> LineCol {
109        let mut byte = usize::from(offset).min(self.source_len);
110        while !source.is_char_boundary(byte) {
111            byte = byte.saturating_sub(1);
112        }
113        let line_idx = match self.line_start_bytes.binary_search(&byte) {
114            Ok(idx) => idx,
115            Err(idx) => idx.saturating_sub(1),
116        };
117        let line_start = self.line_start_bytes[line_idx];
118        LineCol {
119            line: line_idx + 1,
120            column: source[line_start..byte].chars().count() + 1,
121        }
122    }
123
124    #[must_use]
125    pub fn utf16_col(&self, line: usize, byte_col: usize) -> usize {
126        let line_start = self
127            .line_start_bytes
128            .get(line.saturating_sub(1))
129            .copied()
130            .unwrap_or(self.source_len);
131        let byte = line_start
132            .saturating_add(byte_col.saturating_sub(1))
133            .min(self.source_len);
134        self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start]
135    }
136
137    #[must_use]
138    pub fn utf16_range(&self, range: TextRange) -> (usize, usize) {
139        let start = usize::from(range.start()).min(self.source_len);
140        let end = usize::from(range.end()).min(self.source_len).max(start);
141        (
142            self.utf16_offset_by_byte[start],
143            self.utf16_offset_by_byte[end],
144        )
145    }
146}
147
148#[derive(Debug)]
149pub struct SourceTokens {
150    tokens: Vec<Token>,
151    trivia: Vec<Trivia>,
152    lex_errors: Vec<LexError>,
153    laid_out_tokens: OnceLock<Vec<Token>>,
154}
155
156impl SourceTokens {
157    #[must_use]
158    pub fn lex(source: &str) -> Self {
159        let lexed = lex_with_trivia(source);
160        Self {
161            tokens: lexed.tokens,
162            trivia: lexed.trivia,
163            lex_errors: lexed.errors,
164            laid_out_tokens: OnceLock::new(),
165        }
166    }
167
168    #[must_use]
169    pub fn tokens(&self) -> &[Token] {
170        &self.tokens
171    }
172
173    #[must_use]
174    pub fn trivia(&self) -> &[Trivia] {
175        &self.trivia
176    }
177
178    #[must_use]
179    pub fn lex_errors(&self) -> &[LexError] {
180        &self.lex_errors
181    }
182
183    #[must_use]
184    pub fn laid_out_tokens(&self) -> &[Token] {
185        self.laid_out_tokens
186            .get_or_init(|| resolve_layout(self.tokens.clone()))
187    }
188}
189
190#[derive(Debug)]
191pub struct SourceFile {
192    source: String,
193    module: Module,
194    diagnostics: Vec<Diagnostic>,
195    line_index: LineIndex,
196    tokens: OnceLock<SourceTokens>,
197}
198
199impl SourceFile {
200    #[must_use]
201    pub fn parse(source: &str) -> Self {
202        let parsed = parse_module(source);
203        let line_index = LineIndex::new(source);
204        let diagnostics = parsed
205            .diagnostics
206            .into_iter()
207            .map(|diagnostic| {
208                let range = try_parser_span_to_text_range(source, diagnostic.span)
209                    .expect("parser span in diagnostic must map to source bytes");
210                let start = range.start();
211                let end_column = source
212                    .get(usize::from(range.start())..usize::from(range.end()))
213                    .filter(|s| !s.is_empty() && !s.contains('\n'))
214                    .map(|s| diagnostic.pos.column + s.chars().count());
215                Diagnostic {
216                    range,
217                    line: line_index.char_line_col(source, start).line,
218                    column: diagnostic.pos.column,
219                    end_column,
220                    message: diagnostic.message,
221                    category: diagnostic.category,
222                }
223            })
224            .collect();
225
226        Self {
227            source: source.to_string(),
228            module: parsed.module,
229            diagnostics,
230            line_index,
231            tokens: OnceLock::new(),
232        }
233    }
234
235    #[must_use]
236    pub fn source(&self) -> &str {
237        &self.source
238    }
239
240    #[must_use]
241    pub const fn module(&self) -> &Module {
242        &self.module
243    }
244
245    #[must_use]
246    pub fn diagnostics(&self) -> &[Diagnostic] {
247        &self.diagnostics
248    }
249
250    #[must_use]
251    pub const fn line_index(&self) -> &LineIndex {
252        &self.line_index
253    }
254
255    #[must_use]
256    pub fn tokens(&self) -> &[Token] {
257        self.source_tokens().tokens()
258    }
259
260    #[must_use]
261    pub fn trivia(&self) -> &[Trivia] {
262        self.source_tokens().trivia()
263    }
264
265    #[must_use]
266    pub fn laid_out_tokens(&self) -> &[Token] {
267        self.source_tokens().laid_out_tokens()
268    }
269
270    #[must_use]
271    pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
272        self.try_parser_span_to_text_range(span)
273            .expect("parser span must map to a valid UTF-8 range in source")
274    }
275
276    pub fn try_parser_span_to_text_range(
277        &self,
278        span: ParserSpan,
279    ) -> Result<TextRange, ParserSpanToTextRangeError> {
280        try_parser_span_to_text_range(&self.source, span)
281    }
282
283    fn source_tokens(&self) -> &SourceTokens {
284        self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
285    }
286}
287
288#[must_use]
289pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
290    try_parser_span_to_text_range(source, span)
291        .expect("parser span must map to a valid UTF-8 range")
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct ParserSpanToTextRangeError {
296    source_len: usize,
297    span_start: usize,
298    span_end: usize,
299}
300
301impl std::fmt::Display for ParserSpanToTextRangeError {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        write!(
304            f,
305            "parser span [{}, {}) is invalid for source length {}",
306            self.span_start, self.span_end, self.source_len
307        )
308    }
309}
310
311impl std::error::Error for ParserSpanToTextRangeError {}
312
313pub fn try_parser_span_to_text_range(
314    source: &str,
315    span: ParserSpan,
316) -> Result<TextRange, ParserSpanToTextRangeError> {
317    let source_len = source.len();
318    if span.start > source_len || span.end > source_len || span.start > span.end {
319        return Err(ParserSpanToTextRangeError {
320            source_len,
321            span_start: span.start,
322            span_end: span.end,
323        });
324    }
325    Ok(TextRange::new(
326        TextSize::try_from(span.start).map_err(|_| ParserSpanToTextRangeError {
327            source_len,
328            span_start: span.start,
329            span_end: span.end,
330        })?,
331        TextSize::try_from(span.end).map_err(|_| ParserSpanToTextRangeError {
332            source_len,
333            span_start: span.start,
334            span_end: span.end,
335        })?,
336    ))
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use daml_parser::ast_span::render_from_ast;
343    use daml_parser::lexer::render_lossless;
344
345    #[test]
346    fn maps_empty_source_to_first_line() {
347        let index = LineIndex::new("");
348
349        assert_eq!(index.line_col(0.into()), LineCol { line: 1, column: 1 });
350        assert_eq!(index.utf16_range(TextRange::empty(0.into())), (0, 0));
351    }
352
353    #[test]
354    fn maps_ascii_byte_lines() {
355        let source = "module M where\nfoo = 1\n";
356        let index = LineIndex::new(source);
357
358        assert_eq!(index.line_col(15.into()), LineCol { line: 2, column: 1 });
359        assert_eq!(index.utf16_col(2, 4), 3);
360    }
361
362    #[test]
363    fn maps_utf8_and_utf16_offsets() {
364        let source = "a😀b\nz";
365        let index = LineIndex::new(source);
366
367        assert_eq!(
368            index.utf16_range(TextRange::new(0.into(), 6.into())),
369            (0, 4)
370        );
371        assert_eq!(index.utf16_col(1, 6), 3);
372        assert_eq!(
373            index.char_line_col(source, 5.into()),
374            LineCol { line: 1, column: 3 }
375        );
376    }
377
378    #[test]
379    fn char_line_col_snaps_to_previous_utf8_boundary() {
380        let source = "a😀b";
381        let index = LineIndex::new(source);
382
383        // Offset 3 is inside the 4-byte 😀 sequence (1..5), so we expect snapping to 1.
384        assert_eq!(
385            index.char_line_col(source, 3.into()),
386            LineCol { line: 1, column: 2 }
387        );
388    }
389
390    #[test]
391    fn preserves_trailing_newline_line_start() {
392        let index = LineIndex::new("a\n");
393
394        assert_eq!(index.line_col(2.into()), LineCol { line: 2, column: 1 });
395    }
396
397    #[test]
398    fn treats_crlf_as_bytes_without_normalization() {
399        let index = LineIndex::new("a\r\nb");
400
401        assert_eq!(index.line_col(3.into()), LineCol { line: 2, column: 1 });
402    }
403
404    #[test]
405    fn clamps_ranges_to_source_end() {
406        let index = LineIndex::new("abc");
407        let range = TextRange::new(1.into(), 99.into());
408
409        assert_eq!(index.utf16_range(range), (1, 3));
410    }
411
412    #[test]
413    fn source_file_exposes_parser_pipeline_facts() {
414        let source = "module M where\nfoo : Int\nfoo = 1\n";
415        let file = SourceFile::parse(source);
416
417        assert_eq!(file.source(), source);
418        assert_eq!(file.module().name, "M");
419        assert!(file.diagnostics().is_empty());
420        assert!(!file.tokens().is_empty());
421        assert!(!file.laid_out_tokens().is_empty());
422        assert_eq!(
423            render_lossless(source, file.tokens(), file.trivia()).as_deref(),
424            Ok(source)
425        );
426        assert_eq!(
427            render_from_ast(source, file.module(), file.trivia()).as_deref(),
428            Ok(source)
429        );
430    }
431
432    #[test]
433    fn source_tokens_exposes_lex_only_pipeline_facts() {
434        let source = "module M where\nfoo : Int\nfoo = 1\n";
435        let tokens = SourceTokens::lex(source);
436
437        assert!(tokens.lex_errors().is_empty());
438        assert!(!tokens.tokens().is_empty());
439        assert!(!tokens.laid_out_tokens().is_empty());
440        assert_eq!(
441            render_lossless(source, tokens.tokens(), tokens.trivia()).as_deref(),
442            Ok(source)
443        );
444    }
445
446    #[test]
447    fn malformed_source_keeps_source_file_and_diagnostics() {
448        let file = SourceFile::parse("module M where\nfoo = \"unterminated\nbar = 1\n");
449
450        assert_eq!(file.module().name, "M");
451        assert!(file
452            .diagnostics()
453            .iter()
454            .any(|diagnostic| diagnostic.category == DiagnosticCategory::Lex));
455    }
456
457    #[test]
458    fn converts_parser_spans_to_text_ranges() {
459        let file = SourceFile::parse("module M where\nfoo = 1\n");
460        let source_len = file.source().len();
461        let range = file.parser_span_to_text_range(ParserSpan::new(0, source_len));
462
463        assert_eq!(
464            range,
465            TextRange::new(0.into(), source_len.try_into().unwrap())
466        );
467    }
468
469    #[test]
470    fn try_parser_span_to_text_range_rejects_out_of_bounds_spans() {
471        let source = "module M where\nfoo = 1\n";
472        let err = try_parser_span_to_text_range(source, ParserSpan::new(0, source.len() + 1))
473            .unwrap_err();
474        assert_eq!(
475            err.to_string(),
476            format!(
477                "parser span [0, {}) is invalid for source length {}",
478                source.len() + 1,
479                source.len()
480            )
481        );
482    }
483
484    #[test]
485    fn try_parser_span_to_text_range_reports_inverted_spans() {
486        let source = "abc";
487        let err = try_parser_span_to_text_range(source, ParserSpan::new(2, 1)).unwrap_err();
488        assert_eq!(
489            err.to_string(),
490            "parser span [2, 1) is invalid for source length 3"
491        );
492    }
493
494    #[test]
495    fn try_parser_span_to_text_range_succeeds_for_valid_span() {
496        let source = "module M where\nfoo = 1\n";
497        let range = try_parser_span_to_text_range(source, ParserSpan::new(0, 5))
498            .expect("span should be valid");
499        assert_eq!(range, TextRange::new(0.into(), 5.into()));
500    }
501}