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 utf16_offset_by_byte: Vec<usize>,
53}
54
55impl LineIndex {
56 #[must_use]
57 pub fn new(source: &str) -> Self {
58 let mut line_start_bytes = vec![0];
59 for (idx, byte) in source.bytes().enumerate() {
60 if byte == b'\n' {
61 line_start_bytes.push(idx + 1);
62 }
63 }
64
65 let mut utf16_offset_by_byte = vec![0; source.len() + 1];
66 let mut utf16 = 0usize;
67 let mut prev = 0usize;
68 for (idx, ch) in source.char_indices() {
69 for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
70 *slot = utf16;
71 }
72 let char_end = idx + ch.len_utf8();
73 for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
74 *slot = utf16;
75 }
76 utf16 += ch.len_utf16();
77 prev = char_end;
78 }
79 for slot in utf16_offset_by_byte
80 .iter_mut()
81 .take(source.len() + 1)
82 .skip(prev)
83 {
84 *slot = utf16;
85 }
86
87 Self {
88 source_len: source.len(),
89 line_start_bytes,
90 utf16_offset_by_byte,
91 }
92 }
93
94 #[must_use]
95 pub fn line_col(&self, offset: TextSize) -> LineCol {
96 let byte = usize::from(offset).min(self.source_len);
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 LineCol {
102 line: line_idx + 1,
103 column: byte - self.line_start_bytes[line_idx] + 1,
104 }
105 }
106
107 #[must_use]
108 pub fn char_line_col(&self, source: &str, offset: TextSize) -> LineCol {
109 let mut byte = usize::from(offset).min(self.source_len);
110 while !source.is_char_boundary(byte) {
111 byte = byte.saturating_sub(1);
112 }
113 let line_idx = match self.line_start_bytes.binary_search(&byte) {
114 Ok(idx) => idx,
115 Err(idx) => idx.saturating_sub(1),
116 };
117 let line_start = self.line_start_bytes[line_idx];
118 LineCol {
119 line: line_idx + 1,
120 column: source[line_start..byte].chars().count() + 1,
121 }
122 }
123
124 #[must_use]
125 pub fn utf16_col(&self, line: usize, byte_col: usize) -> usize {
126 let line_start = self
127 .line_start_bytes
128 .get(line.saturating_sub(1))
129 .copied()
130 .unwrap_or(self.source_len);
131 let byte = line_start
132 .saturating_add(byte_col.saturating_sub(1))
133 .min(self.source_len);
134 self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start]
135 }
136
137 #[must_use]
138 pub fn utf16_range(&self, range: TextRange) -> (usize, usize) {
139 let start = usize::from(range.start()).min(self.source_len);
140 let end = usize::from(range.end()).min(self.source_len).max(start);
141 (
142 self.utf16_offset_by_byte[start],
143 self.utf16_offset_by_byte[end],
144 )
145 }
146}
147
148#[derive(Debug)]
149pub struct SourceTokens {
150 tokens: Vec<Token>,
151 trivia: Vec<Trivia>,
152 lex_errors: Vec<LexError>,
153 laid_out_tokens: OnceLock<Vec<Token>>,
154}
155
156impl SourceTokens {
157 #[must_use]
158 pub fn lex(source: &str) -> Self {
159 let lexed = lex_with_trivia(source);
160 Self {
161 tokens: lexed.tokens,
162 trivia: lexed.trivia,
163 lex_errors: lexed.errors,
164 laid_out_tokens: OnceLock::new(),
165 }
166 }
167
168 #[must_use]
169 pub fn tokens(&self) -> &[Token] {
170 &self.tokens
171 }
172
173 #[must_use]
174 pub fn trivia(&self) -> &[Trivia] {
175 &self.trivia
176 }
177
178 #[must_use]
179 pub fn lex_errors(&self) -> &[LexError] {
180 &self.lex_errors
181 }
182
183 #[must_use]
184 pub fn laid_out_tokens(&self) -> &[Token] {
185 self.laid_out_tokens
186 .get_or_init(|| resolve_layout(self.tokens.clone()))
187 }
188}
189
190#[derive(Debug)]
191pub struct SourceFile {
192 source: String,
193 module: Module,
194 diagnostics: Vec<Diagnostic>,
195 line_index: LineIndex,
196 tokens: OnceLock<SourceTokens>,
197}
198
199impl SourceFile {
200 #[must_use]
201 pub fn parse(source: &str) -> Self {
202 let parsed = parse_module(source);
203 let line_index = LineIndex::new(source);
204 let diagnostics = parsed
205 .diagnostics
206 .into_iter()
207 .map(|diagnostic| {
208 let range = try_parser_span_to_text_range(source, diagnostic.span)
209 .expect("parser span in diagnostic must map to source bytes");
210 let start = range.start();
211 let end_column = source
212 .get(usize::from(range.start())..usize::from(range.end()))
213 .filter(|s| !s.is_empty() && !s.contains('\n'))
214 .map(|s| diagnostic.pos.column + s.chars().count());
215 Diagnostic {
216 range,
217 line: line_index.char_line_col(source, start).line,
218 column: diagnostic.pos.column,
219 end_column,
220 message: diagnostic.message,
221 category: diagnostic.category,
222 }
223 })
224 .collect();
225
226 Self {
227 source: source.to_string(),
228 module: parsed.module,
229 diagnostics,
230 line_index,
231 tokens: OnceLock::new(),
232 }
233 }
234
235 #[must_use]
236 pub fn source(&self) -> &str {
237 &self.source
238 }
239
240 #[must_use]
241 pub const fn module(&self) -> &Module {
242 &self.module
243 }
244
245 #[must_use]
246 pub fn diagnostics(&self) -> &[Diagnostic] {
247 &self.diagnostics
248 }
249
250 #[must_use]
251 pub const fn line_index(&self) -> &LineIndex {
252 &self.line_index
253 }
254
255 #[must_use]
256 pub fn tokens(&self) -> &[Token] {
257 self.source_tokens().tokens()
258 }
259
260 #[must_use]
261 pub fn trivia(&self) -> &[Trivia] {
262 self.source_tokens().trivia()
263 }
264
265 #[must_use]
266 pub fn laid_out_tokens(&self) -> &[Token] {
267 self.source_tokens().laid_out_tokens()
268 }
269
270 #[must_use]
271 pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
272 self.try_parser_span_to_text_range(span)
273 .expect("parser span must map to a valid UTF-8 range in source")
274 }
275
276 pub fn try_parser_span_to_text_range(
277 &self,
278 span: ParserSpan,
279 ) -> Result<TextRange, ParserSpanToTextRangeError> {
280 try_parser_span_to_text_range(&self.source, span)
281 }
282
283 fn source_tokens(&self) -> &SourceTokens {
284 self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
285 }
286}
287
288#[must_use]
289pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
290 try_parser_span_to_text_range(source, span)
291 .expect("parser span must map to a valid UTF-8 range")
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct ParserSpanToTextRangeError {
296 source_len: usize,
297 span_start: usize,
298 span_end: usize,
299}
300
301impl std::fmt::Display for ParserSpanToTextRangeError {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 write!(
304 f,
305 "parser span [{}, {}) is invalid for source length {}",
306 self.span_start, self.span_end, self.source_len
307 )
308 }
309}
310
311impl std::error::Error for ParserSpanToTextRangeError {}
312
313pub fn try_parser_span_to_text_range(
314 source: &str,
315 span: ParserSpan,
316) -> Result<TextRange, ParserSpanToTextRangeError> {
317 let source_len = source.len();
318 if span.start > source_len || span.end > source_len || span.start > span.end {
319 return Err(ParserSpanToTextRangeError {
320 source_len,
321 span_start: span.start,
322 span_end: span.end,
323 });
324 }
325 Ok(TextRange::new(
326 TextSize::try_from(span.start).map_err(|_| ParserSpanToTextRangeError {
327 source_len,
328 span_start: span.start,
329 span_end: span.end,
330 })?,
331 TextSize::try_from(span.end).map_err(|_| ParserSpanToTextRangeError {
332 source_len,
333 span_start: span.start,
334 span_end: span.end,
335 })?,
336 ))
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use daml_parser::ast_span::render_from_ast;
343 use daml_parser::lexer::render_lossless;
344
345 #[test]
346 fn maps_empty_source_to_first_line() {
347 let index = LineIndex::new("");
348
349 assert_eq!(index.line_col(0.into()), LineCol { line: 1, column: 1 });
350 assert_eq!(index.utf16_range(TextRange::empty(0.into())), (0, 0));
351 }
352
353 #[test]
354 fn maps_ascii_byte_lines() {
355 let source = "module M where\nfoo = 1\n";
356 let index = LineIndex::new(source);
357
358 assert_eq!(index.line_col(15.into()), LineCol { line: 2, column: 1 });
359 assert_eq!(index.utf16_col(2, 4), 3);
360 }
361
362 #[test]
363 fn maps_utf8_and_utf16_offsets() {
364 let source = "a😀b\nz";
365 let index = LineIndex::new(source);
366
367 assert_eq!(
368 index.utf16_range(TextRange::new(0.into(), 6.into())),
369 (0, 4)
370 );
371 assert_eq!(index.utf16_col(1, 6), 3);
372 assert_eq!(
373 index.char_line_col(source, 5.into()),
374 LineCol { line: 1, column: 3 }
375 );
376 }
377
378 #[test]
379 fn char_line_col_snaps_to_previous_utf8_boundary() {
380 let source = "a😀b";
381 let index = LineIndex::new(source);
382
383 assert_eq!(
385 index.char_line_col(source, 3.into()),
386 LineCol { line: 1, column: 2 }
387 );
388 }
389
390 #[test]
391 fn preserves_trailing_newline_line_start() {
392 let index = LineIndex::new("a\n");
393
394 assert_eq!(index.line_col(2.into()), LineCol { line: 2, column: 1 });
395 }
396
397 #[test]
398 fn treats_crlf_as_bytes_without_normalization() {
399 let index = LineIndex::new("a\r\nb");
400
401 assert_eq!(index.line_col(3.into()), LineCol { line: 2, column: 1 });
402 }
403
404 #[test]
405 fn clamps_ranges_to_source_end() {
406 let index = LineIndex::new("abc");
407 let range = TextRange::new(1.into(), 99.into());
408
409 assert_eq!(index.utf16_range(range), (1, 3));
410 }
411
412 #[test]
413 fn source_file_exposes_parser_pipeline_facts() {
414 let source = "module M where\nfoo : Int\nfoo = 1\n";
415 let file = SourceFile::parse(source);
416
417 assert_eq!(file.source(), source);
418 assert_eq!(file.module().name, "M");
419 assert!(file.diagnostics().is_empty());
420 assert!(!file.tokens().is_empty());
421 assert!(!file.laid_out_tokens().is_empty());
422 assert_eq!(
423 render_lossless(source, file.tokens(), file.trivia()).as_deref(),
424 Ok(source)
425 );
426 assert_eq!(
427 render_from_ast(source, file.module(), file.trivia()).as_deref(),
428 Ok(source)
429 );
430 }
431
432 #[test]
433 fn source_tokens_exposes_lex_only_pipeline_facts() {
434 let source = "module M where\nfoo : Int\nfoo = 1\n";
435 let tokens = SourceTokens::lex(source);
436
437 assert!(tokens.lex_errors().is_empty());
438 assert!(!tokens.tokens().is_empty());
439 assert!(!tokens.laid_out_tokens().is_empty());
440 assert_eq!(
441 render_lossless(source, tokens.tokens(), tokens.trivia()).as_deref(),
442 Ok(source)
443 );
444 }
445
446 #[test]
447 fn malformed_source_keeps_source_file_and_diagnostics() {
448 let file = SourceFile::parse("module M where\nfoo = \"unterminated\nbar = 1\n");
449
450 assert_eq!(file.module().name, "M");
451 assert!(file
452 .diagnostics()
453 .iter()
454 .any(|diagnostic| diagnostic.category == DiagnosticCategory::Lex));
455 }
456
457 #[test]
458 fn converts_parser_spans_to_text_ranges() {
459 let file = SourceFile::parse("module M where\nfoo = 1\n");
460 let source_len = file.source().len();
461 let range = file.parser_span_to_text_range(ParserSpan::new(0, source_len));
462
463 assert_eq!(
464 range,
465 TextRange::new(0.into(), source_len.try_into().unwrap())
466 );
467 }
468
469 #[test]
470 fn try_parser_span_to_text_range_rejects_out_of_bounds_spans() {
471 let source = "module M where\nfoo = 1\n";
472 let err = try_parser_span_to_text_range(source, ParserSpan::new(0, source.len() + 1))
473 .unwrap_err();
474 assert_eq!(
475 err.to_string(),
476 format!(
477 "parser span [0, {}) is invalid for source length {}",
478 source.len() + 1,
479 source.len()
480 )
481 );
482 }
483
484 #[test]
485 fn try_parser_span_to_text_range_reports_inverted_spans() {
486 let source = "abc";
487 let err = try_parser_span_to_text_range(source, ParserSpan::new(2, 1)).unwrap_err();
488 assert_eq!(
489 err.to_string(),
490 "parser span [2, 1) is invalid for source length 3"
491 );
492 }
493
494 #[test]
495 fn try_parser_span_to_text_range_succeeds_for_valid_span() {
496 let source = "module M where\nfoo = 1\n";
497 let range = try_parser_span_to_text_range(source, ParserSpan::new(0, 5))
498 .expect("span should be valid");
499 assert_eq!(range, TextRange::new(0.into(), 5.into()));
500 }
501}