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