1pub mod depth;
15pub mod index;
16
17use std::fmt::Write as _;
18use std::ops::Range;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct FileId(pub u32);
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct Span {
30 pub file: FileId,
31 pub start: u32,
32 pub end: u32,
33}
34
35impl Default for Span {
36 fn default() -> Span {
38 Span::NONE
39 }
40}
41
42impl Span {
43 pub const NONE: Span = Span {
44 file: FileId(u32::MAX),
45 start: 0,
46 end: 0,
47 };
48
49 pub fn new(file: FileId, range: Range<usize>) -> Span {
50 Span {
51 file,
52 start: range.start as u32,
53 end: range.end as u32,
54 }
55 }
56
57 pub fn is_none(&self) -> bool {
58 self.file.0 == u32::MAX
59 }
60
61 pub fn to(self, other: Span) -> Span {
63 if self.is_none() {
64 return other;
65 }
66 if other.is_none() || other.file != self.file {
67 return self;
68 }
69 Span {
70 file: self.file,
71 start: self.start.min(other.start),
72 end: self.end.max(other.end),
73 }
74 }
75}
76
77#[derive(Clone, Debug, Default)]
78pub struct SourceMap {
79 files: Vec<SourceFile>,
80}
81
82#[derive(Clone, Debug)]
83struct SourceFile {
84 name: String,
85 text: String,
86 line_starts: Vec<u32>,
88}
89
90impl SourceMap {
91 pub fn new() -> SourceMap {
92 SourceMap::default()
93 }
94
95 pub fn add(&mut self, name: impl Into<String>, text: impl Into<String>) -> FileId {
96 let (name, text) = (name.into(), text.into());
97 let mut line_starts = vec![0u32];
98 for (i, b) in text.bytes().enumerate() {
99 if b == b'\n' {
100 line_starts.push(i as u32 + 1);
101 }
102 }
103 self.files.push(SourceFile {
104 name,
105 text,
106 line_starts,
107 });
108 FileId(self.files.len() as u32 - 1)
109 }
110
111 pub fn name(&self, file: FileId) -> &str {
112 &self.files[file.0 as usize].name
113 }
114
115 pub fn find(&self, name: &str) -> Option<FileId> {
123 self.files
124 .iter()
125 .position(|f| f.name == name)
126 .map(|i| FileId(i as u32))
127 }
128
129 pub fn text(&self, file: FileId) -> &str {
130 &self.files[file.0 as usize].text
131 }
132
133 pub fn line_col(&self, file: FileId, offset: u32) -> (usize, usize) {
139 let Some(f) = self.files.get(file.0 as usize) else {
140 return (1, 1);
141 };
142 let line = f.line_starts.partition_point(|&s| s <= offset).max(1) - 1;
143 let start = f.line_starts[line] as usize;
144 let col = f.text[start..(offset as usize).min(f.text.len())]
145 .chars()
146 .count();
147 (line + 1, col + 1)
148 }
149
150 fn line_text(&self, file: FileId, line: usize) -> &str {
151 let f = &self.files[file.0 as usize];
152 let start = f.line_starts[line - 1] as usize;
153 let end = f
154 .line_starts
155 .get(line)
156 .map(|&e| e as usize - 1)
157 .unwrap_or(f.text.len());
158 f.text[start..end.min(f.text.len())].trim_end_matches('\r')
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
163pub enum Severity {
164 Error,
165 Warning,
166 Note,
167}
168
169impl Severity {
170 fn label(self) -> &'static str {
171 match self {
172 Severity::Error => "error",
173 Severity::Warning => "warning",
174 Severity::Note => "note",
175 }
176 }
177}
178
179#[derive(Clone, Debug)]
181pub struct Label {
182 pub span: Span,
183 pub message: String,
184}
185
186#[derive(Clone, Debug)]
188pub struct ExpansionStep {
189 pub macro_name: String,
190 pub span: Span,
191}
192
193#[derive(Clone, Debug)]
194pub struct Diagnostic {
195 pub severity: Severity,
196 pub code: &'static str,
198 pub message: String,
199 pub primary: Span,
200 pub primary_label: Option<String>,
201 pub labels: Vec<Label>,
202 pub notes: Vec<String>,
203 pub fix: Option<String>,
206 pub expansion: Vec<ExpansionStep>,
207}
208
209impl Diagnostic {
210 pub fn error(code: &'static str, message: impl Into<String>, primary: Span) -> Diagnostic {
211 Diagnostic {
212 severity: Severity::Error,
213 code,
214 message: message.into(),
215 primary,
216 primary_label: None,
217 labels: Vec::new(),
218 notes: Vec::new(),
219 fix: None,
220 expansion: Vec::new(),
221 }
222 }
223
224 pub fn warning(code: &'static str, message: impl Into<String>, primary: Span) -> Diagnostic {
225 Diagnostic {
226 severity: Severity::Warning,
227 ..Diagnostic::error(code, message, primary)
228 }
229 }
230
231 pub fn with_primary_label(mut self, label: impl Into<String>) -> Diagnostic {
232 self.primary_label = Some(label.into());
233 self
234 }
235
236 pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Diagnostic {
237 self.labels.push(Label {
238 span,
239 message: message.into(),
240 });
241 self
242 }
243
244 pub fn with_note(mut self, note: impl Into<String>) -> Diagnostic {
245 self.notes.push(note.into());
246 self
247 }
248
249 pub fn with_fix(mut self, fix: impl Into<String>) -> Diagnostic {
250 self.fix = Some(fix.into());
251 self
252 }
253}
254
255#[derive(Clone, Debug, Default)]
257pub struct Diagnostics {
258 items: Vec<Diagnostic>,
259}
260
261impl Diagnostics {
262 pub fn new() -> Diagnostics {
263 Diagnostics::default()
264 }
265
266 pub fn push(&mut self, d: Diagnostic) {
267 self.items.push(d);
268 }
269
270 pub fn extend(&mut self, other: Diagnostics) {
271 self.items.extend(other.items);
272 }
273
274 pub fn has_errors(&self) -> bool {
275 self.items.iter().any(|d| d.severity == Severity::Error)
276 }
277
278 pub fn is_empty(&self) -> bool {
279 self.items.is_empty()
280 }
281
282 pub fn len(&self) -> usize {
283 self.items.len()
284 }
285
286 pub fn truncate(&mut self, mark: usize) {
294 self.items.truncate(mark);
295 }
296
297 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
298 self.items.iter()
299 }
300
301 pub fn sorted(&self) -> Vec<&Diagnostic> {
303 let mut out: Vec<&Diagnostic> = self.items.iter().collect();
304 out.sort_by_key(|d| (d.primary.file, d.primary.start, d.code));
305 out
306 }
307
308 pub fn render(&self, map: &SourceMap) -> String {
309 let mut out = String::new();
310 for d in self.sorted() {
311 out.push_str(&render(d, map));
312 out.push('\n');
313 }
314 out
315 }
316}
317
318pub fn render(d: &Diagnostic, map: &SourceMap) -> String {
321 let mut out = String::new();
322 let _ = writeln!(out, "{}[{}]: {}", d.severity.label(), d.code, d.message);
323
324 if !d.primary.is_none() {
325 let (line, col) = map.line_col(d.primary.file, d.primary.start);
326 let _ = writeln!(out, " --> {}:{}:{}", map.name(d.primary.file), line, col);
327 render_snippet(&mut out, map, d.primary, d.primary_label.as_deref());
328 }
329
330 for label in &d.labels {
331 if label.span.is_none() {
332 continue;
333 }
334 let (line, col) = map.line_col(label.span.file, label.span.start);
335 let _ = writeln!(out, " --> {}:{}:{}", map.name(label.span.file), line, col);
336 render_snippet(&mut out, map, label.span, Some(&label.message));
337 }
338
339 for step in &d.expansion {
340 if step.span.is_none() {
341 let _ = writeln!(out, " = in `{}`", step.macro_name);
342 } else {
343 let (line, col) = map.line_col(step.span.file, step.span.start);
344 let _ = writeln!(
345 out,
346 " = in `{}` expanded at {}:{}:{}",
347 step.macro_name,
348 map.name(step.span.file),
349 line,
350 col
351 );
352 }
353 }
354
355 for note in &d.notes {
356 let _ = writeln!(out, " = note: {note}");
357 }
358 if let Some(fix) = &d.fix {
359 let _ = writeln!(out, " = help: {fix}");
360 }
361 out
362}
363
364fn render_snippet(out: &mut String, map: &SourceMap, span: Span, label: Option<&str>) {
365 let (line, col) = map.line_col(span.file, span.start);
366 let text = map.line_text(span.file, line);
367 let gutter = line.to_string();
368 let pad = " ".repeat(gutter.len());
369
370 let _ = writeln!(out, "{pad} |");
371 let _ = writeln!(out, "{gutter} | {text}");
372
373 let line_end = span.start + (text.chars().count() as u32 - (col as u32 - 1));
376 let end = span.end.min(line_end);
377 let width = map
378 .text(span.file)
379 .get(span.start as usize..end as usize)
380 .map(|s| s.chars().count())
381 .unwrap_or(1)
382 .max(1);
383
384 let _ = write!(out, "{pad} | {}{}", " ".repeat(col - 1), "^".repeat(width));
385 match label {
386 Some(l) => {
387 let _ = writeln!(out, " {l}");
388 }
389 None => {
390 let _ = writeln!(out);
391 }
392 }
393 let _ = writeln!(out, "{pad} |");
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn line_col_is_one_based_and_counts_characters() {
402 let mut map = SourceMap::new();
403 let f = map.add("t.beck", "def f():\n return é + 1\n");
404 assert_eq!(map.line_col(f, 0), (1, 1));
405 assert_eq!(map.line_col(f, 9), (2, 1));
406 let idx = map.text(f).find('é').unwrap() as u32;
408 assert_eq!(map.line_col(f, idx), (2, 12));
409 assert_eq!(map.line_col(f, idx + 2), (2, 13));
410 }
411
412 #[test]
413 fn a_rendered_diagnostic_points_at_the_right_column() {
414 let mut map = SourceMap::new();
415 let src = "def f():\n return g(1)\n";
416 let f = map.add("t.beck", src);
417 let span = Span::new(f, 20..21);
418 assert_eq!(&src[20..21], "g");
419 let d = Diagnostic::error("B0001", "cannot find `g` in this scope", span)
420 .with_primary_label("not found")
421 .with_note("`g` is not defined in this module")
422 .with_fix("did you mean `f`?");
423 let rendered = render(&d, &map);
424 assert!(rendered.contains("error[B0001]: cannot find `g` in this scope"));
425 assert!(rendered.contains("--> t.beck:2:12"));
426 assert!(rendered.contains(" ^ not found"));
427 assert!(rendered.contains("= help: did you mean `f`?"));
428 }
429
430 #[test]
431 fn sorting_makes_output_independent_of_check_order() {
432 let mut map = SourceMap::new();
433 let f = map.add("t.beck", "aaa\nbbb\n");
434 let mut ds = Diagnostics::new();
435 ds.push(Diagnostic::error("B0002", "second", Span::new(f, 4..7)));
436 ds.push(Diagnostic::error("B0001", "first", Span::new(f, 0..3)));
437 let codes: Vec<_> = ds.sorted().iter().map(|d| d.code).collect();
438 assert_eq!(codes, ["B0001", "B0002"]);
439 assert!(ds.has_errors());
440 }
441}