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 text(&self, file: FileId) -> &str {
116 &self.files[file.0 as usize].text
117 }
118
119 pub fn snippet(&self, span: Span) -> &str {
120 if span.is_none() {
121 return "";
122 }
123 let text = self.text(span.file);
124 let (s, e) = (span.start as usize, (span.end as usize).min(text.len()));
125 text.get(s..e).unwrap_or("")
126 }
127
128 pub fn line_col(&self, file: FileId, offset: u32) -> (usize, usize) {
134 let Some(f) = self.files.get(file.0 as usize) else {
135 return (1, 1);
136 };
137 let line = f.line_starts.partition_point(|&s| s <= offset).max(1) - 1;
138 let start = f.line_starts[line] as usize;
139 let col = f.text[start..(offset as usize).min(f.text.len())]
140 .chars()
141 .count();
142 (line + 1, col + 1)
143 }
144
145 fn line_text(&self, file: FileId, line: usize) -> &str {
146 let f = &self.files[file.0 as usize];
147 let start = f.line_starts[line - 1] as usize;
148 let end = f
149 .line_starts
150 .get(line)
151 .map(|&e| e as usize - 1)
152 .unwrap_or(f.text.len());
153 f.text[start..end.min(f.text.len())].trim_end_matches('\r')
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
158pub enum Severity {
159 Error,
160 Warning,
161 Note,
162}
163
164impl Severity {
165 fn label(self) -> &'static str {
166 match self {
167 Severity::Error => "error",
168 Severity::Warning => "warning",
169 Severity::Note => "note",
170 }
171 }
172}
173
174#[derive(Clone, Debug)]
176pub struct Label {
177 pub span: Span,
178 pub message: String,
179}
180
181#[derive(Clone, Debug)]
183pub struct ExpansionStep {
184 pub macro_name: String,
185 pub span: Span,
186}
187
188#[derive(Clone, Debug)]
189pub struct Diagnostic {
190 pub severity: Severity,
191 pub code: &'static str,
193 pub message: String,
194 pub primary: Span,
195 pub primary_label: Option<String>,
196 pub labels: Vec<Label>,
197 pub notes: Vec<String>,
198 pub fix: Option<String>,
201 pub expansion: Vec<ExpansionStep>,
202}
203
204impl Diagnostic {
205 pub fn error(code: &'static str, message: impl Into<String>, primary: Span) -> Diagnostic {
206 Diagnostic {
207 severity: Severity::Error,
208 code,
209 message: message.into(),
210 primary,
211 primary_label: None,
212 labels: Vec::new(),
213 notes: Vec::new(),
214 fix: None,
215 expansion: Vec::new(),
216 }
217 }
218
219 pub fn warning(code: &'static str, message: impl Into<String>, primary: Span) -> Diagnostic {
220 Diagnostic {
221 severity: Severity::Warning,
222 ..Diagnostic::error(code, message, primary)
223 }
224 }
225
226 pub fn with_primary_label(mut self, label: impl Into<String>) -> Diagnostic {
227 self.primary_label = Some(label.into());
228 self
229 }
230
231 pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Diagnostic {
232 self.labels.push(Label {
233 span,
234 message: message.into(),
235 });
236 self
237 }
238
239 pub fn with_note(mut self, note: impl Into<String>) -> Diagnostic {
240 self.notes.push(note.into());
241 self
242 }
243
244 pub fn with_fix(mut self, fix: impl Into<String>) -> Diagnostic {
245 self.fix = Some(fix.into());
246 self
247 }
248
249 pub fn with_expansion(mut self, steps: Vec<ExpansionStep>) -> Diagnostic {
250 self.expansion = steps;
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 iter(&self) -> impl Iterator<Item = &Diagnostic> {
287 self.items.iter()
288 }
289
290 pub fn sorted(&self) -> Vec<&Diagnostic> {
292 let mut out: Vec<&Diagnostic> = self.items.iter().collect();
293 out.sort_by_key(|d| (d.primary.file, d.primary.start, d.code));
294 out
295 }
296
297 pub fn render(&self, map: &SourceMap) -> String {
298 let mut out = String::new();
299 for d in self.sorted() {
300 out.push_str(&render(d, map));
301 out.push('\n');
302 }
303 out
304 }
305}
306
307pub fn render(d: &Diagnostic, map: &SourceMap) -> String {
310 let mut out = String::new();
311 let _ = writeln!(out, "{}[{}]: {}", d.severity.label(), d.code, d.message);
312
313 if !d.primary.is_none() {
314 let (line, col) = map.line_col(d.primary.file, d.primary.start);
315 let _ = writeln!(out, " --> {}:{}:{}", map.name(d.primary.file), line, col);
316 render_snippet(&mut out, map, d.primary, d.primary_label.as_deref());
317 }
318
319 for label in &d.labels {
320 if label.span.is_none() {
321 continue;
322 }
323 let (line, col) = map.line_col(label.span.file, label.span.start);
324 let _ = writeln!(out, " --> {}:{}:{}", map.name(label.span.file), line, col);
325 render_snippet(&mut out, map, label.span, Some(&label.message));
326 }
327
328 for step in &d.expansion {
329 if step.span.is_none() {
330 let _ = writeln!(out, " = in `{}`", step.macro_name);
331 } else {
332 let (line, col) = map.line_col(step.span.file, step.span.start);
333 let _ = writeln!(
334 out,
335 " = in `{}` expanded at {}:{}:{}",
336 step.macro_name,
337 map.name(step.span.file),
338 line,
339 col
340 );
341 }
342 }
343
344 for note in &d.notes {
345 let _ = writeln!(out, " = note: {note}");
346 }
347 if let Some(fix) = &d.fix {
348 let _ = writeln!(out, " = help: {fix}");
349 }
350 out
351}
352
353fn render_snippet(out: &mut String, map: &SourceMap, span: Span, label: Option<&str>) {
354 let (line, col) = map.line_col(span.file, span.start);
355 let text = map.line_text(span.file, line);
356 let gutter = line.to_string();
357 let pad = " ".repeat(gutter.len());
358
359 let _ = writeln!(out, "{pad} |");
360 let _ = writeln!(out, "{gutter} | {text}");
361
362 let line_end = span.start + (text.chars().count() as u32 - (col as u32 - 1));
365 let end = span.end.min(line_end);
366 let width = map
367 .text(span.file)
368 .get(span.start as usize..end as usize)
369 .map(|s| s.chars().count())
370 .unwrap_or(1)
371 .max(1);
372
373 let _ = write!(out, "{pad} | {}{}", " ".repeat(col - 1), "^".repeat(width));
374 match label {
375 Some(l) => {
376 let _ = writeln!(out, " {l}");
377 }
378 None => {
379 let _ = writeln!(out);
380 }
381 }
382 let _ = writeln!(out, "{pad} |");
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn line_col_is_one_based_and_counts_characters() {
391 let mut map = SourceMap::new();
392 let f = map.add("t.beck", "def f():\n return é + 1\n");
393 assert_eq!(map.line_col(f, 0), (1, 1));
394 assert_eq!(map.line_col(f, 9), (2, 1));
395 let idx = map.text(f).find('é').unwrap() as u32;
397 assert_eq!(map.line_col(f, idx), (2, 12));
398 assert_eq!(map.line_col(f, idx + 2), (2, 13));
399 }
400
401 #[test]
402 fn a_rendered_diagnostic_points_at_the_right_column() {
403 let mut map = SourceMap::new();
404 let src = "def f():\n return g(1)\n";
405 let f = map.add("t.beck", src);
406 let span = Span::new(f, 20..21);
407 assert_eq!(&src[20..21], "g");
408 let d = Diagnostic::error("B0001", "cannot find `g` in this scope", span)
409 .with_primary_label("not found")
410 .with_note("`g` is not defined in this module")
411 .with_fix("did you mean `f`?");
412 let rendered = render(&d, &map);
413 assert!(rendered.contains("error[B0001]: cannot find `g` in this scope"));
414 assert!(rendered.contains("--> t.beck:2:12"));
415 assert!(rendered.contains(" ^ not found"));
416 assert!(rendered.contains("= help: did you mean `f`?"));
417 }
418
419 #[test]
420 fn sorting_makes_output_independent_of_check_order() {
421 let mut map = SourceMap::new();
422 let f = map.add("t.beck", "aaa\nbbb\n");
423 let mut ds = Diagnostics::new();
424 ds.push(Diagnostic::error("B0002", "second", Span::new(f, 4..7)));
425 ds.push(Diagnostic::error("B0001", "first", Span::new(f, 0..3)));
426 let codes: Vec<_> = ds.sorted().iter().map(|d| d.code).collect();
427 assert_eq!(codes, ["B0001", "B0002"]);
428 assert!(ds.has_errors());
429 }
430}