1use daml_parser::ast::{DiagnosticCategory, Module, Span as ParserSpan};
41use daml_parser::layout::resolve_layout;
42use daml_parser::lexer::{lex_with_trivia, LexError, Token, Trivia};
43use daml_parser::parse::parse_module;
44use std::sync::OnceLock;
45
46pub mod coordinate;
47
48pub use coordinate::{
49 ByteColumn, ByteLineCol, ByteOffset, CharColumn, CharLineCol, Coordinate, LineNumber,
50 Utf16Offset,
51};
52pub use text_size::{TextRange, TextSize};
53
54#[derive(Debug, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct Diagnostic {
61 range: TextRange,
62 line: LineNumber,
63 column: CharColumn,
64 end_column: Option<CharColumn>,
65 message: String,
66 category: DiagnosticCategory,
67}
68
69impl Diagnostic {
70 #[must_use]
72 pub const fn range(&self) -> TextRange {
73 self.range
74 }
75
76 #[must_use]
78 pub const fn line(&self) -> LineNumber {
79 self.line
80 }
81
82 #[must_use]
84 pub const fn column(&self) -> CharColumn {
85 self.column
86 }
87
88 #[must_use]
90 pub const fn end_column(&self) -> Option<CharColumn> {
91 self.end_column
92 }
93
94 #[must_use]
96 pub fn message(&self) -> &str {
97 &self.message
98 }
99
100 #[must_use]
102 pub const fn category(&self) -> DiagnosticCategory {
103 self.category
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct LineIndex {
113 source_len: usize,
114 line_start_bytes: Vec<usize>,
115 char_offset_by_byte: Vec<usize>,
116 utf16_offset_by_byte: Vec<usize>,
117}
118
119impl LineIndex {
120 #[must_use]
122 pub fn new(source: &str) -> Self {
123 let mut line_start_bytes = vec![0];
124 for (idx, byte) in source.bytes().enumerate() {
125 if byte == b'\n' {
126 line_start_bytes.push(idx + 1);
127 }
128 }
129
130 let mut char_offset_by_byte = vec![0; source.len() + 1];
131 let mut char_count = 0usize;
132 let mut prev = 0usize;
133 for (idx, ch) in source.char_indices() {
134 for slot in char_offset_by_byte.iter_mut().take(idx).skip(prev) {
135 *slot = char_count;
136 }
137 let char_end = idx + ch.len_utf8();
138 for slot in char_offset_by_byte.iter_mut().take(char_end).skip(idx) {
139 *slot = char_count;
140 }
141 char_count += 1;
142 prev = char_end;
143 }
144 for slot in char_offset_by_byte
145 .iter_mut()
146 .take(source.len() + 1)
147 .skip(prev)
148 {
149 *slot = char_count;
150 }
151
152 let mut utf16_offset_by_byte = vec![0; source.len() + 1];
153 let mut utf16 = 0usize;
154 let mut prev = 0usize;
155 for (idx, ch) in source.char_indices() {
156 for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
157 *slot = utf16;
158 }
159 let char_end = idx + ch.len_utf8();
160 for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
161 *slot = utf16;
162 }
163 utf16 += ch.len_utf16();
164 prev = char_end;
165 }
166 for slot in utf16_offset_by_byte
167 .iter_mut()
168 .take(source.len() + 1)
169 .skip(prev)
170 {
171 *slot = utf16;
172 }
173
174 Self {
175 source_len: source.len(),
176 line_start_bytes,
177 char_offset_by_byte,
178 utf16_offset_by_byte,
179 }
180 }
181
182 #[must_use]
184 pub fn line_col(&self, offset: TextSize) -> ByteLineCol {
185 let byte = usize::from(offset).min(self.source_len);
186 let line_idx = match self.line_start_bytes.binary_search(&byte) {
187 Ok(idx) => idx,
188 Err(idx) => idx.saturating_sub(1),
189 };
190 ByteLineCol {
191 line: LineNumber::new(line_idx + 1),
192 column: ByteColumn::new(byte - self.line_start_bytes[line_idx] + 1),
193 }
194 }
195
196 #[must_use]
200 pub fn char_line_col(&self, offset: TextSize) -> CharLineCol {
201 let byte = usize::from(offset).min(self.source_len);
202 let line_idx = match self.line_start_bytes.binary_search(&byte) {
203 Ok(idx) => idx,
204 Err(idx) => idx.saturating_sub(1),
205 };
206 let line_start = self.line_start_bytes[line_idx];
207 CharLineCol {
208 line: LineNumber::new(line_idx + 1),
209 column: CharColumn::new(
210 self.char_offset_by_byte[byte] - self.char_offset_by_byte[line_start] + 1,
211 ),
212 }
213 }
214
215 #[must_use]
220 pub fn utf16_col(&self, line: LineNumber, byte_col: ByteColumn) -> Utf16Offset {
221 let line_start = self
222 .line_start_bytes
223 .get(line.get().saturating_sub(1))
224 .copied()
225 .unwrap_or(self.source_len);
226 let byte = line_start
227 .saturating_add(byte_col.get().saturating_sub(1))
228 .min(self.source_len);
229 Utf16Offset::new(self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start])
230 }
231
232 #[must_use]
234 pub fn utf16_range(&self, range: TextRange) -> (Utf16Offset, Utf16Offset) {
235 let start = usize::from(range.start()).min(self.source_len);
236 let end = usize::from(range.end()).min(self.source_len).max(start);
237 (
238 Utf16Offset::new(self.utf16_offset_by_byte[start]),
239 Utf16Offset::new(self.utf16_offset_by_byte[end]),
240 )
241 }
242}
243
244#[derive(Debug)]
248pub struct SourceTokens {
249 tokens: Vec<Token>,
250 trivia: Vec<Trivia>,
251 lex_errors: Vec<LexError>,
252 laid_out_tokens: OnceLock<Vec<Token>>,
253}
254
255impl SourceTokens {
256 #[must_use]
258 pub fn lex(source: &str) -> Self {
259 let lexed = lex_with_trivia(source);
260 Self {
261 tokens: lexed.tokens,
262 trivia: lexed.trivia,
263 lex_errors: lexed.errors,
264 laid_out_tokens: OnceLock::new(),
265 }
266 }
267
268 #[must_use]
270 pub fn tokens(&self) -> &[Token] {
271 &self.tokens
272 }
273
274 #[must_use]
276 pub fn trivia(&self) -> &[Trivia] {
277 &self.trivia
278 }
279
280 #[must_use]
282 pub fn lex_errors(&self) -> &[LexError] {
283 &self.lex_errors
284 }
285
286 #[must_use]
288 pub fn laid_out_tokens(&self) -> &[Token] {
289 self.laid_out_tokens
290 .get_or_init(|| resolve_layout(self.tokens.clone()))
291 }
292}
293
294#[derive(Debug)]
296pub struct SourceFile {
297 source: String,
298 module: Module,
299 diagnostics: Vec<Diagnostic>,
300 line_index: LineIndex,
301 tokens: OnceLock<SourceTokens>,
302}
303
304impl SourceFile {
305 #[must_use]
310 pub fn parse(source: &str) -> Self {
311 let parsed = parse_module(source);
312 let line_index = LineIndex::new(source);
313 let diagnostics = parsed
314 .diagnostics
315 .into_iter()
316 .map(|diagnostic| {
317 let range = try_parser_span_to_text_range(source, diagnostic.span)
318 .expect("parser span in diagnostic must map to source bytes");
319 let start = range.start();
320 let end_column = source
321 .get(usize::from(range.start())..usize::from(range.end()))
322 .filter(|s| !s.is_empty() && !s.contains('\n'))
323 .map(|s| CharColumn::new(diagnostic.pos.column + s.chars().count()));
324 let start_pos = line_index.char_line_col(start);
325 Diagnostic {
326 range,
327 line: start_pos.line,
328 column: CharColumn::new(diagnostic.pos.column),
329 end_column,
330 message: diagnostic.message,
331 category: diagnostic.category,
332 }
333 })
334 .collect();
335
336 Self {
337 source: source.to_string(),
338 module: parsed.module,
339 diagnostics,
340 line_index,
341 tokens: OnceLock::new(),
342 }
343 }
344
345 #[must_use]
347 pub fn source(&self) -> &str {
348 &self.source
349 }
350
351 #[must_use]
353 pub const fn module(&self) -> &Module {
354 &self.module
355 }
356
357 #[must_use]
359 pub fn diagnostics(&self) -> &[Diagnostic] {
360 &self.diagnostics
361 }
362
363 #[must_use]
365 pub const fn line_index(&self) -> &LineIndex {
366 &self.line_index
367 }
368
369 #[must_use]
371 pub fn tokens(&self) -> &[Token] {
372 self.source_tokens().tokens()
373 }
374
375 #[must_use]
377 pub fn trivia(&self) -> &[Trivia] {
378 self.source_tokens().trivia()
379 }
380
381 #[must_use]
383 pub fn laid_out_tokens(&self) -> &[Token] {
384 self.source_tokens().laid_out_tokens()
385 }
386
387 #[must_use]
397 pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
398 self.try_parser_span_to_text_range(span)
399 .expect("parser span must map to a valid UTF-8 range in source")
400 }
401
402 #[must_use = "handle invalid span offsets before using the range"]
410 pub fn try_parser_span_to_text_range(
411 &self,
412 span: ParserSpan,
413 ) -> Result<TextRange, ParserSpanToTextRangeError> {
414 try_parser_span_to_text_range(&self.source, span)
415 }
416
417 fn source_tokens(&self) -> &SourceTokens {
418 self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
419 }
420}
421
422#[must_use]
432pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
433 try_parser_span_to_text_range(source, span)
434 .expect("parser span must map to a valid UTF-8 range")
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439#[non_exhaustive]
440pub enum ParserSpanToTextRangeErrorKind {
441 OutOfBounds,
443 InvertedSpan,
445 NonUtf8Boundary,
447 TextSizeOverflow,
449}
450
451#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct ParserSpanToTextRangeError {
454 source_len: usize,
455 span_start: ByteOffset,
456 span_end: ByteOffset,
457 kind: ParserSpanToTextRangeErrorKind,
458}
459
460impl ParserSpanToTextRangeError {
461 #[must_use]
463 pub const fn source_len(&self) -> usize {
464 self.source_len
465 }
466
467 #[must_use]
469 pub const fn span_start(&self) -> ByteOffset {
470 self.span_start
471 }
472
473 #[must_use]
475 pub const fn span_end(&self) -> ByteOffset {
476 self.span_end
477 }
478
479 #[must_use]
481 pub fn span_start_usize(&self) -> usize {
482 self.span_start.get()
483 }
484
485 #[must_use]
487 pub fn span_end_usize(&self) -> usize {
488 self.span_end.get()
489 }
490
491 #[must_use]
493 pub const fn kind(&self) -> ParserSpanToTextRangeErrorKind {
494 self.kind
495 }
496}
497
498impl std::fmt::Display for ParserSpanToTextRangeError {
499 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500 match self.kind {
501 ParserSpanToTextRangeErrorKind::TextSizeOverflow => write!(
502 f,
503 "parser span [{}, {}) cannot be represented as a text-size value",
504 self.span_start.get(),
505 self.span_end.get()
506 ),
507 _ => write!(
508 f,
509 "parser span [{}, {}) is invalid for source length {}",
510 self.span_start.get(),
511 self.span_end.get(),
512 self.source_len
513 ),
514 }
515 }
516}
517
518impl std::error::Error for ParserSpanToTextRangeError {}
519
520#[must_use = "handle invalid span offsets before converting"]
526pub fn try_parser_span_to_text_range(
527 source: &str,
528 span: ParserSpan,
529) -> Result<TextRange, ParserSpanToTextRangeError> {
530 let source_len = source.len();
531 if span.start > source_len || span.end > source_len {
532 return Err(ParserSpanToTextRangeError {
533 source_len,
534 span_start: ByteOffset::new(span.start),
535 span_end: ByteOffset::new(span.end),
536 kind: ParserSpanToTextRangeErrorKind::OutOfBounds,
537 });
538 }
539 if span.start > span.end {
540 return Err(ParserSpanToTextRangeError {
541 source_len,
542 span_start: ByteOffset::new(span.start),
543 span_end: ByteOffset::new(span.end),
544 kind: ParserSpanToTextRangeErrorKind::InvertedSpan,
545 });
546 }
547 if !source.is_char_boundary(span.start) || !source.is_char_boundary(span.end) {
548 return Err(ParserSpanToTextRangeError {
549 source_len,
550 span_start: ByteOffset::new(span.start),
551 span_end: ByteOffset::new(span.end),
552 kind: ParserSpanToTextRangeErrorKind::NonUtf8Boundary,
553 });
554 }
555 Ok(TextRange::new(
556 TextSize::try_from(span.start).map_err(|_| ParserSpanToTextRangeError {
557 source_len,
558 span_start: ByteOffset::new(span.start),
559 span_end: ByteOffset::new(span.end),
560 kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
561 })?,
562 TextSize::try_from(span.end).map_err(|_| ParserSpanToTextRangeError {
563 source_len,
564 span_start: ByteOffset::new(span.start),
565 span_end: ByteOffset::new(span.end),
566 kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
567 })?,
568 ))
569}
570
571#[doc(hidden)]
573mod readme_examples {
574 }
590
591#[cfg(test)]
592#[allow(clippy::unwrap_used)]
593mod tests {
594 use super::*;
595 use daml_parser::ast_span::render_from_ast;
596 use daml_parser::lexer::render_lossless;
597
598 #[test]
599 fn maps_empty_source_to_first_line() {
600 let index = LineIndex::new("");
601
602 assert_eq!(
603 index.line_col(0.into()),
604 ByteLineCol {
605 line: LineNumber::new(1),
606 column: ByteColumn::new(1),
607 }
608 );
609 assert_eq!(
610 index.utf16_range(TextRange::empty(0.into())),
611 (Utf16Offset::new(0), Utf16Offset::new(0))
612 );
613 }
614
615 #[test]
616 fn maps_ascii_byte_lines() {
617 let source = "module M where\nfoo = 1\n";
618 let index = LineIndex::new(source);
619
620 assert_eq!(
621 index.line_col(15.into()),
622 ByteLineCol {
623 line: LineNumber::new(2),
624 column: ByteColumn::new(1),
625 }
626 );
627 assert_eq!(
628 index.utf16_col(LineNumber::new(2), ByteColumn::new(4)),
629 Utf16Offset::new(3)
630 );
631 }
632
633 #[test]
634 fn maps_utf8_and_utf16_offsets() {
635 let source = "a😀b\nz";
636 let index = LineIndex::new(source);
637
638 assert_eq!(
639 index.utf16_range(TextRange::new(0.into(), 6.into())),
640 (Utf16Offset::new(0), Utf16Offset::new(4))
641 );
642 assert_eq!(
643 index.utf16_col(LineNumber::new(1), ByteColumn::new(6)),
644 Utf16Offset::new(3)
645 );
646 assert_eq!(
647 index.char_line_col(5.into()),
648 CharLineCol {
649 line: LineNumber::new(1),
650 column: CharColumn::new(3),
651 }
652 );
653 }
654
655 #[test]
656 fn char_line_col_snaps_to_previous_utf8_boundary() {
657 let source = "a😀b";
658 let index = LineIndex::new(source);
659
660 assert_eq!(
662 index.char_line_col(3.into()),
663 CharLineCol {
664 line: LineNumber::new(1),
665 column: CharColumn::new(2),
666 }
667 );
668 }
669
670 #[test]
671 fn preserves_trailing_newline_line_start() {
672 let index = LineIndex::new("a\n");
673
674 assert_eq!(
675 index.line_col(2.into()),
676 ByteLineCol {
677 line: LineNumber::new(2),
678 column: ByteColumn::new(1),
679 }
680 );
681 }
682
683 #[test]
684 fn treats_crlf_as_bytes_without_normalization() {
685 let index = LineIndex::new("a\r\nb");
686
687 assert_eq!(
688 index.line_col(3.into()),
689 ByteLineCol {
690 line: LineNumber::new(2),
691 column: ByteColumn::new(1),
692 }
693 );
694 }
695
696 #[test]
697 fn clamps_ranges_to_source_end() {
698 let index = LineIndex::new("abc");
699 let range = TextRange::new(1.into(), 99.into());
700
701 assert_eq!(
702 index.utf16_range(range),
703 (Utf16Offset::new(1), Utf16Offset::new(3))
704 );
705 }
706
707 #[test]
708 fn byte_and_char_line_col_columns_differ_for_multibyte_utf8() {
709 let source = "😀\n";
710 let index = LineIndex::new(source);
711 let offset = TextSize::from(4);
712
713 let byte_pos = index.line_col(offset);
714 let char_pos = index.char_line_col(offset);
715
716 assert_eq!(byte_pos.line, char_pos.line);
717 assert_ne!(byte_pos.column.get(), char_pos.column.get());
718 assert_eq!(byte_pos.column, ByteColumn::new(5));
719 assert_eq!(char_pos.column, CharColumn::new(2));
720 }
721
722 #[test]
723 fn source_file_exposes_parser_pipeline_facts() {
724 let source = "module M where\nfoo : Int\nfoo = 1\n";
725 let file = SourceFile::parse(source);
726
727 assert_eq!(file.source(), source);
728 assert_eq!(file.module().name, "M");
729 assert!(file.diagnostics().is_empty());
730 assert!(!file.tokens().is_empty());
731 assert!(!file.laid_out_tokens().is_empty());
732 assert_eq!(
733 render_lossless(source, file.tokens(), file.trivia()).as_deref(),
734 Ok(source)
735 );
736 assert_eq!(
737 render_from_ast(source, file.module(), file.trivia()).as_deref(),
738 Ok(source)
739 );
740 }
741
742 #[test]
743 fn source_tokens_exposes_lex_only_pipeline_facts() {
744 let source = "module M where\nfoo : Int\nfoo = 1\n";
745 let tokens = SourceTokens::lex(source);
746
747 assert!(tokens.lex_errors().is_empty());
748 assert!(!tokens.tokens().is_empty());
749 assert!(!tokens.laid_out_tokens().is_empty());
750 assert_eq!(
751 render_lossless(source, tokens.tokens(), tokens.trivia()).as_deref(),
752 Ok(source)
753 );
754 }
755
756 #[test]
757 fn malformed_source_keeps_source_file_and_diagnostics() {
758 let file = SourceFile::parse("module M where\nfoo = \"unterminated\nbar = 1\n");
759
760 assert_eq!(file.module().name, "M");
761 assert!(file
762 .diagnostics()
763 .iter()
764 .any(|diagnostic| diagnostic.category() == DiagnosticCategory::Lex));
765 }
766
767 #[test]
768 fn converts_parser_spans_to_text_ranges() {
769 let file = SourceFile::parse("module M where\nfoo = 1\n");
770 let source_len = file.source().len();
771 let range = file.parser_span_to_text_range(ParserSpan::new(0, source_len));
772
773 assert_eq!(
774 range,
775 TextRange::new(0.into(), source_len.try_into().unwrap())
776 );
777 }
778
779 #[test]
780 fn try_parser_span_to_text_range_rejects_out_of_bounds_spans() {
781 let source = "module M where\nfoo = 1\n";
782 let err = try_parser_span_to_text_range(source, ParserSpan::new(0, source.len() + 1))
783 .unwrap_err();
784 assert_eq!(err.kind(), ParserSpanToTextRangeErrorKind::OutOfBounds);
785 assert_eq!(
786 err.to_string(),
787 format!(
788 "parser span [0, {}) is invalid for source length {}",
789 source.len() + 1,
790 source.len()
791 )
792 );
793 assert_eq!(err.source_len(), source.len());
794 assert_eq!(err.span_start(), ByteOffset::new(0));
795 assert_eq!(err.span_end(), ByteOffset::new(source.len() + 1));
796 assert_eq!(err.span_start_usize(), 0);
797 assert_eq!(err.span_end_usize(), source.len() + 1);
798 }
799
800 #[test]
801 fn try_parser_span_to_text_range_reports_inverted_spans() {
802 let source = "abc";
803 let err = try_parser_span_to_text_range(source, ParserSpan::new(2, 1)).unwrap_err();
804 assert_eq!(err.kind(), ParserSpanToTextRangeErrorKind::InvertedSpan);
805 assert_eq!(
806 err.to_string(),
807 "parser span [2, 1) is invalid for source length 3"
808 );
809 assert_eq!(err.source_len(), source.len());
810 assert_eq!(err.span_start(), ByteOffset::new(2));
811 assert_eq!(err.span_end(), ByteOffset::new(1));
812 }
813
814 #[test]
815 fn try_parser_span_to_text_range_rejects_non_utf8_boundary_spans() {
816 let source = "a😀b";
817 let err = try_parser_span_to_text_range(source, ParserSpan::new(1, 2)).unwrap_err();
818 assert_eq!(err.kind(), ParserSpanToTextRangeErrorKind::NonUtf8Boundary);
819
820 assert_eq!(
821 err.to_string(),
822 "parser span [1, 2) is invalid for source length 6"
823 );
824 assert_eq!(err.source_len(), source.len());
825 assert_eq!(err.span_start(), ByteOffset::new(1));
826 assert_eq!(err.span_end(), ByteOffset::new(2));
827 }
828
829 #[test]
830 fn diagnostics_are_read_through_accessors_not_field_literals() {
831 let file = SourceFile::parse("module M where\nfoo = \"unterminated\n");
832
833 let diagnostic = file
834 .diagnostics()
835 .first()
836 .expect("malformed source should surface diagnostics");
837 assert_eq!(diagnostic.category(), DiagnosticCategory::Lex);
838 assert!(!diagnostic.message().is_empty());
839 assert!(diagnostic.range().start() <= diagnostic.range().end());
840 assert!(diagnostic.line().get() >= 1);
841 assert!(diagnostic.column().get() >= 1);
842 }
843
844 #[test]
845 fn utf16_col_requires_non_zero_line_and_column() {
846 assert!(LineNumber::try_new(0).is_none());
847 assert!(ByteColumn::try_new(0).is_none());
848 }
849
850 #[test]
851 fn try_parser_span_to_text_range_succeeds_for_valid_span() {
852 let source = "module M where\nfoo = 1\n";
853 let range = try_parser_span_to_text_range(source, ParserSpan::new(0, 5))
854 .expect("span should be valid");
855 assert_eq!(range, TextRange::new(0.into(), 5.into()));
856 }
857}