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