spade_diagnostics/lib.rs
1//! This crate is all about constructing structured [`Diagnostic`]s and emitting
2//! them in some specified format.
3//!
4//! At the time of writing we are in the process of porting old diagnostics to
5//! these structured diagnostics, which is [tracked in spade#190 on
6//! GitLab](https://gitlab.com/spade-lang/spade/-/issues/190).
7//!
8//! ## Diagnostics
9//!
10//! [`Diagnostic`]s are created using builders. The simplest compiler error looks like this:
11//!
12//! ```
13//! # use spade_codespan_reporting::term::termcolor::Buffer;
14//! # use spade_diagnostics::{CodeBundle, Diagnostic};
15//! # use spade_diagnostics::emitter::{CodespanEmitter, Emitter};
16//! # let mut emitter = CodespanEmitter;
17//! # let mut buffer = Buffer::no_color();
18//! let code = CodeBundle::new("hello ocean!".to_string());
19//! // Spans are never created manually like this. They are created by the lexer
20//! // and need to be combined with each other to form bigger spans.
21//! let span = (spade_codespan::Span::from(6..11), 0);
22//! let diag = Diagnostic::error(span, "something's fishy :spadesquint:");
23//! emitter.emit_diagnostic(&diag, &mut buffer, &code);
24//! # println!("{}", String::from_utf8_lossy(buffer.as_slice()));
25//! # // for takin' a peek at the output:
26//! # // println!("{}", String::from_utf8_lossy(buffer.as_slice())); panic!();
27//! ```
28//!
29//! ```text
30//! error: something's fishy :spadesquint:
31//! ┌─ <str>:1:7
32//! │
33//! 1 │ hello ocean!
34//! │ ^^^^^
35//! ```
36//!
37//! As mentioned, spans shouldn't be created manually. They are passed on from
38//! earlier stages in the compiler, all the way from the tokenizer, usually as a
39//! [`Loc<T>`]. Additional Locs can then be created by combining earlier Locs, for
40//! example using [`between_locs`]. The rest of this documentation will assume that
41//! Locs and code exist, and instead focus on creating diagnostics. The examples
42//! will be inspired by diagnostics currently emitted by spade.
43//!
44//! [`Loc<T>`]: spade_common::location_info::Loc
45//! [`between_locs`]: spade_common::location_info::WithLocation::between_locs
46//!
47//! ### Secondary labels
48//!
49//! ```text
50//! fn foo() {}
51//! ^^^ ------------- first_foo
52//! fn bar() {}
53//!
54//! fn foo() {}
55//! ^^^ ------------- second_foo
56//! ```
57//!
58//! ```
59//! # use spade_codespan_reporting::term::termcolor::Buffer;
60//! # use spade_common::location_info::WithLocation;
61//! # use spade_diagnostics::{CodeBundle, Diagnostic};
62//! # use spade_diagnostics::emitter::{CodespanEmitter, Emitter};
63//! # let mut emitter = CodespanEmitter;
64//! # let mut buffer = Buffer::no_color();
65//! # let code = CodeBundle::new(r#"fn foo() {}
66//! #
67//! # fn bar() {}
68//! #
69//! # fn foo() {}
70//! # "#.to_string());
71//! # let first_foo = ().at(0, &(4..7));
72//! # let second_foo = ().at(0, &(30..33));
73//! # let diag =
74//! Diagnostic::error(second_foo, "Duplicate definition of item")
75//! .primary_label("Second definition here")
76//! .secondary_label(first_foo, "First definition here")
77//! # ;
78//! # emitter.emit_diagnostic(&diag, &mut buffer, &code);
79//! # // println!("{}", String::from_utf8_lossy(buffer.as_slice())); panic!();
80//! ```
81//!
82//! ```text
83//! error: Duplicate definition of item
84//! ┌─ <str>:5:4
85//! │
86//! 1 │ fn foo() {}
87//! │ --- First definition here
88//! ·
89//! 5 │ fn foo() {}
90//! │ ^^^ Second definition here
91//! ```
92//!
93//! Note that labels are sorted by location in the code automatically.
94//!
95//! ### Notes and spanned notes
96//!
97//! Notes are additional snippets of text that are shown after the labels. They
98//! can be used to give additional information or help notices.
99//!
100//! ```text
101//! fn foo(port: &int<10>) {}
102//! ^^^^^^^^ ------ port_ty
103//! ```
104//!
105//! ```
106//! # use spade_codespan_reporting::term::termcolor::Buffer;
107//! # use spade_common::location_info::WithLocation;
108//! # use spade_diagnostics::{CodeBundle, Diagnostic};
109//! # use spade_diagnostics::emitter::{CodespanEmitter, Emitter};
110//! # let mut emitter = CodespanEmitter;
111//! # let mut buffer = Buffer::no_color();
112//! # let code = CodeBundle::new(r#"fn foo(port: &int<10>) {}"#.to_string());
113//! # let port_ty = ().at(0, &(13..21));
114//! # let diag =
115//! Diagnostic::error(port_ty, "Port argument in function")
116//! .primary_label("This is a port")
117//! .note("Only entities and pipelines can take ports as arguments")
118//! # ;
119//! # emitter.emit_diagnostic(&diag, &mut buffer, &code);
120//! # // println!("{}", String::from_utf8_lossy(buffer.as_slice())); panic!();
121//! ```
122//!
123//! ```text
124//! error: Port argument in function
125//! ┌─ <str>:1:14
126//! │
127//! 1 │ fn foo(port: &int<10>) {}
128//! │ ^^^^^^ This is a port
129//! │
130//! = note: Only entities and pipelines can take ports as arguments
131//! ```
132//!
133//! The spanned versions are rendered like labels, but compared to the secondary
134//! labels they are always rendered separately.
135//!
136//! ### Suggestions
137//!
138//! Suggestions can be used to format a change suggestion to the user. There are a
139//! bunch of convenience functions depending on what kind of suggestion is needed.
140//! Try to use them since they show the intent behind the suggestion.
141//!
142//! ```text
143//! struct port S {
144//! ^^^^ --------------- port_kw
145//! field1: &bool,
146//! field2: bool,
147//! ^^^^^^ ^^^^ ---------- field_ty
148//! |----------------- field
149//! }
150//! ```
151//!
152//! ```
153//! # use spade_codespan_reporting::term::termcolor::Buffer;
154//! # use spade_common::location_info::WithLocation;
155//! # use spade_diagnostics::{CodeBundle, Diagnostic};
156//! # use spade_diagnostics::emitter::{CodespanEmitter, Emitter};
157//! # let mut emitter = CodespanEmitter;
158//! # let mut buffer = Buffer::no_color();
159//! # let code = CodeBundle::new(r#"struct port S {
160//! # field1: &bool,
161//! # field2: bool,
162//! # }
163//! # "#.to_string());
164//! # let port_kw = ().at(0, &(7..11));
165//! # let field_ty = ().at(0, &(47..51));
166//! # let field = "field2".at(0, &(39..45));
167//! # let diag =
168//! Diagnostic::error(field_ty, "Non-port in port struct")
169//! .primary_label("This is not a port type")
170//! .secondary_label(port_kw, "This is a port struct")
171//! .note("All members of a port struct must be ports")
172//! .span_suggest_insert_before(
173//! format!("Consider making {field} a wire"),
174//! field_ty,
175//! "&",
176//! )
177//! # ;
178//! # emitter.emit_diagnostic(&diag, &mut buffer, &code);
179//! # // println!("{}", String::from_utf8_lossy(buffer.as_slice())); panic!();
180//! ```
181//!
182//! ```text
183//! error: Non-port in port struct
184//! ┌─ <str>:3:13
185//! │
186//! 1 │ struct port S {
187//! │ ---- This is a port struct
188//! 2 │ field1: &bool,
189//! 3 │ field2: bool,
190//! │ ^^^^ This is not a port type
191//! │
192//! = note: All members of a port struct must be ports
193//! = Consider making field2 a wire
194//! │
195//! 3 │ field2: &bool,
196//! │ +
197//! ```
198//!
199//! The convenience functions start with `span_suggest` and include
200//! [`span_suggest_insert_before`] (above), [`span_suggest_replace`] and
201//! [`span_suggest_remove`].
202//!
203//! [`span_suggest_insert_before`]: Diagnostic::span_suggest_insert_before
204//! [`span_suggest_replace`]: Diagnostic::span_suggest_replace
205//! [`span_suggest_remove`]: Diagnostic::span_suggest_remove
206//!
207//! Multipart suggestions are basically multiple single part suggestions. Use them
208//! when the suggestion needs to include changes that are separated in the code.
209//!
210//! ```text
211//! enum E {
212//! VariantA(a: bool),
213//! ^ ^ ---- close_paren
214//! |------------- open_paren
215//! }
216//! ```
217//!
218//! ```
219//! # use spade_codespan_reporting::term::termcolor::Buffer;
220//! # use spade_common::location_info::WithLocation;
221//! # use spade_diagnostics::{CodeBundle, Diagnostic};
222//! # use spade_diagnostics::diagnostic::{SuggestionParts};
223//! # use spade_diagnostics::emitter::{CodespanEmitter, Emitter};
224//! # let mut emitter = CodespanEmitter;
225//! # let mut buffer = Buffer::no_color();
226//! # let code = CodeBundle::new(r#"enum E {
227//! # VariantA(a: bool),
228//! # }
229//! # "#.to_string());
230//! # let open_paren = ().at(0, &(21..22));
231//! # let close_paren = ().at(0, &(29..30));
232//! # let diag =
233//! Diagnostic::error(open_paren, "Expected '{', '}' or ','")
234//! .span_suggest_multipart(
235//! "Use '{' if you want to add items to this enum variant",
236//! SuggestionParts::new()
237//! .part(open_paren, "{")
238//! .part(close_paren, "}"),
239//! )
240//! # ;
241//! # emitter.emit_diagnostic(&diag, &mut buffer, &code);
242//! # // println!("{}", String::from_utf8_lossy(buffer.as_slice())); panic!();
243//! ```
244//!
245//! ```text
246//! error: Expected '{', '}' or ','
247//! ┌─ <str>:2:13
248//! │
249//! 2 │ VariantA(a: bool),
250//! │ ^
251//! │
252//! = Use '{' if you want to add items to this enum variant
253//! │
254//! 2 │ VariantA{a: bool},
255//! │ ~ ~
256//! ```
257//!
258//! ## Emitters
259//!
260//! We also need some way to show these diagnostics to the user. For this we have
261//! [`Emitter`]s which abstract away the details of formatting the diagnostic. The
262//! default emitter is the [`CodespanEmitter`] which formats the diagnostics using
263//! a [forked `codespan-reporting`](https://gitlab.com/spade-lang/codespan).
264//!
265//! [`CodespanEmitter`]: emitter::CodespanEmitter
266//!
267//! > If you use the compiler as a library (like we do for the [Spade Language
268//! > Server](https://gitlab.com/spade-lang/spade-language-server/)) you can define
269//! > your own emitter that formats the diagnostics. The language server, for
270//! > example, has [its own
271//! > emitter](https://gitlab.com/spade-lang/spade-language-server/-/blob/5eccf6c71724ec1074f69f535132a5b298d583ba/src/main.rs#L75)
272//! > that sends LSP-friendly diagnostics to the connected Language Server Client.
273//!
274//! When writing diagnostics in the compiler you usually don't have to care about
275//! the emitter. Almost everywhere, the diagnostics are returned and handled by
276//! someone else. (In Spade, that someone else is `spade-compiler`.)
277
278use std::io::Write;
279
280use spade_codespan_reporting::files::{Files, SimpleFiles};
281use spade_codespan_reporting::term::termcolor::Buffer;
282
283use spade_common::location_info::{AsLabel, Loc};
284
285pub use diagnostic::Diagnostic;
286pub use emitter::Emitter;
287pub use spade_codespan as codespan;
288
289pub mod diag_list;
290pub mod diagnostic;
291pub mod emitter;
292
293/// A bundle of all the source code included in the current compilation
294#[derive(Clone)]
295pub struct CodeBundle {
296 pub files: SimpleFiles<String, String>,
297}
298
299impl CodeBundle {
300 // Create a new code bundle adding the passed string as the 0th file
301 pub fn new(string: String) -> Self {
302 let mut files = SimpleFiles::new();
303 files.add("<str>".to_string(), string);
304 Self { files }
305 }
306
307 pub fn from_files(files: &[(String, String)]) -> Self {
308 let mut result = Self {
309 files: SimpleFiles::new(),
310 };
311 for (name, content) in files {
312 result.add_file(name.clone(), content.clone());
313 }
314 result
315 }
316
317 pub fn add_file(&mut self, filename: String, content: String) -> usize {
318 self.files.add(filename, content)
319 }
320
321 pub fn dump_files(&self) -> Vec<(&str, &str)> {
322 let mut all_files = vec![];
323 loop {
324 match self.files.get(all_files.len()) {
325 Ok(file) => all_files.push((file.name().as_str(), file.source().as_str())),
326 Err(spade_codespan_reporting::files::Error::FileMissing) => break,
327 Err(e) => {
328 panic!("{e}")
329 }
330 };
331 }
332 all_files
333 }
334
335 pub fn source_loc<T>(&self, loc: &Loc<T>) -> String {
336 let location = self
337 .files
338 .location(loc.file_id(), loc.span.start().to_usize())
339 .expect("Loc was not in code bundle");
340 format!(
341 "{}:{},{}",
342 self.files.get(loc.file_id()).unwrap().name(),
343 location.line_number,
344 location.column_number
345 )
346 }
347}
348
349pub trait CompilationError {
350 fn report(&self, buffer: &mut Buffer, code: &CodeBundle, diag_handler: &mut DiagHandler);
351}
352
353impl CompilationError for std::io::Error {
354 fn report(&self, buffer: &mut Buffer, _code: &CodeBundle, _diag_handler: &mut DiagHandler) {
355 if let Err(e) = buffer.write_all(self.to_string().as_bytes()) {
356 eprintln!(
357 "io error when writing io error to error buffer\noriginal error: {}\nnew error: {}",
358 self, e
359 );
360 }
361 }
362}
363
364impl CompilationError for Diagnostic {
365 fn report(&self, buffer: &mut Buffer, code: &CodeBundle, diag_handler: &mut DiagHandler) {
366 diag_handler.emit(self, buffer, code)
367 }
368}
369
370pub struct DiagHandler {
371 emitter: Box<dyn Emitter + Send>,
372 // Here we can add more shared state for diagnostics. For example, rustc can
373 // stash diagnostics that can be retrieved in later stages, indexed by (Span, StashKey).
374}
375
376impl DiagHandler {
377 pub fn new(emitter: Box<dyn Emitter + Send>) -> Self {
378 Self { emitter }
379 }
380
381 pub fn emit(&mut self, diagnostic: &Diagnostic, buffer: &mut Buffer, code: &CodeBundle) {
382 self.emitter.emit_diagnostic(diagnostic, buffer, code);
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use spade_codespan_reporting::term::termcolor::Buffer;
389
390 use crate::emitter::CodespanEmitter;
391 use crate::{CodeBundle, Diagnostic, Emitter};
392
393 #[test]
394 fn bug_diagnostics_works() {
395 let code = CodeBundle::new("hello goodbye".to_string());
396 let sp = ((6..13).into(), 0);
397 let mut buffer = Buffer::no_color();
398 let mut emitter = CodespanEmitter;
399 let diagnostic = Diagnostic::bug(sp, "oof");
400 emitter.emit_diagnostic(&diagnostic, &mut buffer, &code);
401 insta::assert_snapshot!(String::from_utf8(buffer.into_inner()).unwrap());
402 }
403}