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