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
8use daml_parser::ast::{Module, Span as ParserSpan};
9use daml_parser::layout::resolve_layout;
10use daml_parser::lexer::{lex_with_trivia, LexError, Token, Trivia};
11use daml_parser::parse::parse_module;
12use std::sync::OnceLock;
13
14pub use text_size::{TextRange, TextSize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct LineCol {
18    pub line: usize,
19    pub column: usize,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Diagnostic {
24    pub range: TextRange,
25    pub line: usize,
26    pub column: usize,
27    pub end_column: Option<usize>,
28    pub message: String,
29    pub category: &'static str,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct LineIndex {
34    source_len: usize,
35    line_start_bytes: Vec<usize>,
36    utf16_offset_by_byte: Vec<usize>,
37}
38
39impl LineIndex {
40    #[must_use]
41    pub fn new(source: &str) -> Self {
42        let mut line_start_bytes = vec![0];
43        for (idx, byte) in source.bytes().enumerate() {
44            if byte == b'\n' {
45                line_start_bytes.push(idx + 1);
46            }
47        }
48
49        let mut utf16_offset_by_byte = vec![0; source.len() + 1];
50        let mut utf16 = 0usize;
51        let mut prev = 0usize;
52        for (idx, ch) in source.char_indices() {
53            for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
54                *slot = utf16;
55            }
56            let char_end = idx + ch.len_utf8();
57            for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
58                *slot = utf16;
59            }
60            utf16 += ch.len_utf16();
61            prev = char_end;
62        }
63        for slot in utf16_offset_by_byte
64            .iter_mut()
65            .take(source.len() + 1)
66            .skip(prev)
67        {
68            *slot = utf16;
69        }
70
71        Self {
72            source_len: source.len(),
73            line_start_bytes,
74            utf16_offset_by_byte,
75        }
76    }
77
78    #[must_use]
79    pub fn line_col(&self, offset: TextSize) -> LineCol {
80        let byte = usize::from(offset).min(self.source_len);
81        let line_idx = match self.line_start_bytes.binary_search(&byte) {
82            Ok(idx) => idx,
83            Err(idx) => idx.saturating_sub(1),
84        };
85        LineCol {
86            line: line_idx + 1,
87            column: byte - self.line_start_bytes[line_idx] + 1,
88        }
89    }
90
91    #[must_use]
92    pub fn char_line_col(&self, source: &str, offset: TextSize) -> LineCol {
93        let mut byte = usize::from(offset).min(self.source_len);
94        while !source.is_char_boundary(byte) {
95            byte = byte.saturating_sub(1);
96        }
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        let line_start = self.line_start_bytes[line_idx];
102        LineCol {
103            line: line_idx + 1,
104            column: source[line_start..byte].chars().count() + 1,
105        }
106    }
107
108    #[must_use]
109    pub fn utf16_col(&self, line: usize, byte_col: usize) -> usize {
110        let line_start = self
111            .line_start_bytes
112            .get(line.saturating_sub(1))
113            .copied()
114            .unwrap_or(self.source_len);
115        let byte = line_start
116            .saturating_add(byte_col.saturating_sub(1))
117            .min(self.source_len);
118        self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start]
119    }
120
121    #[must_use]
122    pub fn utf16_range(&self, range: TextRange) -> (usize, usize) {
123        let start = usize::from(range.start()).min(self.source_len);
124        let end = usize::from(range.end()).min(self.source_len).max(start);
125        (
126            self.utf16_offset_by_byte[start],
127            self.utf16_offset_by_byte[end],
128        )
129    }
130}
131
132#[derive(Debug)]
133pub struct SourceTokens {
134    tokens: Vec<Token>,
135    trivia: Vec<Trivia>,
136    lex_errors: Vec<LexError>,
137    laid_out_tokens: OnceLock<Vec<Token>>,
138}
139
140impl SourceTokens {
141    #[must_use]
142    pub fn lex(source: &str) -> Self {
143        let lexed = lex_with_trivia(source);
144        Self {
145            tokens: lexed.tokens,
146            trivia: lexed.trivia,
147            lex_errors: lexed.errors,
148            laid_out_tokens: OnceLock::new(),
149        }
150    }
151
152    #[must_use]
153    pub fn tokens(&self) -> &[Token] {
154        &self.tokens
155    }
156
157    #[must_use]
158    pub fn trivia(&self) -> &[Trivia] {
159        &self.trivia
160    }
161
162    #[must_use]
163    pub fn lex_errors(&self) -> &[LexError] {
164        &self.lex_errors
165    }
166
167    #[must_use]
168    pub fn laid_out_tokens(&self) -> &[Token] {
169        self.laid_out_tokens
170            .get_or_init(|| resolve_layout(self.tokens.clone()))
171    }
172}
173
174#[derive(Debug)]
175pub struct SourceFile {
176    source: String,
177    module: Module,
178    diagnostics: Vec<Diagnostic>,
179    line_index: LineIndex,
180    tokens: OnceLock<SourceTokens>,
181}
182
183impl SourceFile {
184    #[must_use]
185    pub fn parse(source: &str) -> Self {
186        let parsed = parse_module(source);
187        let line_index = LineIndex::new(source);
188        let diagnostics = parsed
189            .diagnostics
190            .into_iter()
191            .map(|diagnostic| {
192                let range = parser_span_to_text_range(source, diagnostic.span);
193                let start = range.start();
194                let end_column = source
195                    .get(usize::from(range.start())..usize::from(range.end()))
196                    .filter(|s| !s.is_empty() && !s.contains('\n'))
197                    .map(|s| diagnostic.pos.column + s.chars().count());
198                Diagnostic {
199                    range,
200                    line: line_index.char_line_col(source, start).line,
201                    column: diagnostic.pos.column,
202                    end_column,
203                    message: diagnostic.message,
204                    category: diagnostic.category.as_str(),
205                }
206            })
207            .collect();
208
209        Self {
210            source: source.to_string(),
211            module: parsed.module,
212            diagnostics,
213            line_index,
214            tokens: OnceLock::new(),
215        }
216    }
217
218    #[must_use]
219    pub fn source(&self) -> &str {
220        &self.source
221    }
222
223    #[must_use]
224    pub const fn module(&self) -> &Module {
225        &self.module
226    }
227
228    #[must_use]
229    pub fn diagnostics(&self) -> &[Diagnostic] {
230        &self.diagnostics
231    }
232
233    #[must_use]
234    pub const fn line_index(&self) -> &LineIndex {
235        &self.line_index
236    }
237
238    #[must_use]
239    pub fn tokens(&self) -> &[Token] {
240        self.source_tokens().tokens()
241    }
242
243    #[must_use]
244    pub fn trivia(&self) -> &[Trivia] {
245        self.source_tokens().trivia()
246    }
247
248    #[must_use]
249    pub fn laid_out_tokens(&self) -> &[Token] {
250        self.source_tokens().laid_out_tokens()
251    }
252
253    #[must_use]
254    pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
255        parser_span_to_text_range(&self.source, span)
256    }
257
258    fn source_tokens(&self) -> &SourceTokens {
259        self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
260    }
261}
262
263#[must_use]
264pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
265    let start = span.start.min(source.len());
266    let end = span.end.min(source.len()).max(start);
267    TextRange::new(
268        TextSize::try_from(start).unwrap(),
269        TextSize::try_from(end).unwrap(),
270    )
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use daml_parser::ast_span::render_from_ast;
277    use daml_parser::lexer::render_lossless;
278
279    #[test]
280    fn maps_empty_source_to_first_line() {
281        let index = LineIndex::new("");
282
283        assert_eq!(index.line_col(0.into()), LineCol { line: 1, column: 1 });
284        assert_eq!(index.utf16_range(TextRange::empty(0.into())), (0, 0));
285    }
286
287    #[test]
288    fn maps_ascii_byte_lines() {
289        let source = "module M where\nfoo = 1\n";
290        let index = LineIndex::new(source);
291
292        assert_eq!(index.line_col(15.into()), LineCol { line: 2, column: 1 });
293        assert_eq!(index.utf16_col(2, 4), 3);
294    }
295
296    #[test]
297    fn maps_utf8_and_utf16_offsets() {
298        let source = "a😀b\nz";
299        let index = LineIndex::new(source);
300
301        assert_eq!(
302            index.utf16_range(TextRange::new(0.into(), 6.into())),
303            (0, 4)
304        );
305        assert_eq!(index.utf16_col(1, 6), 3);
306        assert_eq!(
307            index.char_line_col(source, 5.into()),
308            LineCol { line: 1, column: 3 }
309        );
310    }
311
312    #[test]
313    fn char_line_col_snaps_to_previous_utf8_boundary() {
314        let source = "a😀b";
315        let index = LineIndex::new(source);
316
317        // Offset 3 is inside the 4-byte 😀 sequence (1..5), so we expect snapping to 1.
318        assert_eq!(
319            index.char_line_col(source, 3.into()),
320            LineCol { line: 1, column: 2 }
321        );
322    }
323
324    #[test]
325    fn preserves_trailing_newline_line_start() {
326        let index = LineIndex::new("a\n");
327
328        assert_eq!(index.line_col(2.into()), LineCol { line: 2, column: 1 });
329    }
330
331    #[test]
332    fn treats_crlf_as_bytes_without_normalization() {
333        let index = LineIndex::new("a\r\nb");
334
335        assert_eq!(index.line_col(3.into()), LineCol { line: 2, column: 1 });
336    }
337
338    #[test]
339    fn clamps_ranges_to_source_end() {
340        let index = LineIndex::new("abc");
341        let range = TextRange::new(1.into(), 99.into());
342
343        assert_eq!(index.utf16_range(range), (1, 3));
344    }
345
346    #[test]
347    fn source_file_exposes_parser_pipeline_facts() {
348        let source = "module M where\nfoo : Int\nfoo = 1\n";
349        let file = SourceFile::parse(source);
350
351        assert_eq!(file.source(), source);
352        assert_eq!(file.module().name, "M");
353        assert!(file.diagnostics().is_empty());
354        assert!(!file.tokens().is_empty());
355        assert!(!file.laid_out_tokens().is_empty());
356        assert_eq!(
357            render_lossless(source, file.tokens(), file.trivia()).as_deref(),
358            Ok(source)
359        );
360        assert_eq!(
361            render_from_ast(source, file.module(), file.trivia()).as_deref(),
362            Ok(source)
363        );
364    }
365
366    #[test]
367    fn source_tokens_exposes_lex_only_pipeline_facts() {
368        let source = "module M where\nfoo : Int\nfoo = 1\n";
369        let tokens = SourceTokens::lex(source);
370
371        assert!(tokens.lex_errors().is_empty());
372        assert!(!tokens.tokens().is_empty());
373        assert!(!tokens.laid_out_tokens().is_empty());
374        assert_eq!(
375            render_lossless(source, tokens.tokens(), tokens.trivia()).as_deref(),
376            Ok(source)
377        );
378    }
379
380    #[test]
381    fn malformed_source_keeps_source_file_and_diagnostics() {
382        let file = SourceFile::parse("module M where\nfoo = \"unterminated\nbar = 1\n");
383
384        assert_eq!(file.module().name, "M");
385        assert!(file
386            .diagnostics()
387            .iter()
388            .any(|diagnostic| diagnostic.category == "lexical-error"));
389    }
390
391    #[test]
392    fn converts_parser_spans_to_text_ranges() {
393        let file = SourceFile::parse("module M where\nfoo = 1\n");
394        let range = file.parser_span_to_text_range(ParserSpan::new(0, 99));
395
396        assert_eq!(
397            range,
398            TextRange::new(0.into(), file.source().len().try_into().unwrap())
399        );
400    }
401}