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