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}