Skip to content

Commit

Permalink
refactor(parser,diagnostic): one diagnostic struct to eliminate monom…
Browse files Browse the repository at this point in the history
…orphization of generic types (#3214)

part of #3213

We should only have one diagnostic struct instead 353 copies of them, so we don't end up choking LLVM with 50k lines of the same code due to monomorphization.

If the proposed approach is good, then I'll start writing a codemod to turn all the existing structs to plain functions.

---

Background:

Using `--timings`, we see `oxc_linter` is slow on codegen (the purple part).

![image](https://github.com/zkat/miette/assets/1430279/c1df4f7d-90ef-4c0f-9956-2ec3194db7ca)

The crate currently contains 353 miette errors. [cargo-llvm-lines](https://github.com/dtolnay/cargo-llvm-lines) displays

```
cargo llvm-lines -p oxc_linter --lib --release

  Lines                 Copies               Function name
  -----                 ------               -------------
  830350                33438                (TOTAL)
   29252 (3.5%,  3.5%)    808 (2.4%,  2.4%)  <alloc::boxed::Box<T,A> as core::ops::drop::Drop>::drop
   23298 (2.8%,  6.3%)    353 (1.1%,  3.5%)  miette::eyreish::error::object_downcast
   19062 (2.3%,  8.6%)    706 (2.1%,  5.6%)  core::error::Error::type_id
   12610 (1.5%, 10.1%)     65 (0.2%,  5.8%)  alloc::raw_vec::RawVec<T,A>::grow_amortized
   12002 (1.4%, 11.6%)    706 (2.1%,  7.9%)  miette::eyreish::ptr::Own<T>::boxed
    9215 (1.1%, 12.7%)    115 (0.3%,  8.2%)  core::iter::traits::iterator::Iterator::try_fold
    9150 (1.1%, 13.8%)      1 (0.0%,  8.2%)  oxc_linter::rules::RuleEnum::read_json
    8825 (1.1%, 14.9%)    353 (1.1%,  9.3%)  <miette::eyreish::error::ErrorImpl<E> as core::error::Error>::source
    8822 (1.1%, 15.9%)    353 (1.1%, 10.3%)  miette::eyreish::error::<impl miette::eyreish::Report>::construct
    8119 (1.0%, 16.9%)    353 (1.1%, 11.4%)  miette::eyreish::error::object_ref
    8119 (1.0%, 17.9%)    353 (1.1%, 12.5%)  miette::eyreish::error::object_ref_stderr
    7413 (0.9%, 18.8%)    353 (1.1%, 13.5%)  <miette::eyreish::error::ErrorImpl<E> as core::fmt::Display>::fmt
    7413 (0.9%, 19.7%)    353 (1.1%, 14.6%)  miette::eyreish::ptr::Own<T>::new
    6669 (0.8%, 20.5%)     39 (0.1%, 14.7%)  alloc::raw_vec::RawVec<T,A>::try_allocate_in
    6173 (0.7%, 21.2%)    353 (1.1%, 15.7%)  miette::eyreish::error::<impl miette::eyreish::Report>::from_std
    6027 (0.7%, 21.9%)     70 (0.2%, 16.0%)  <alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter
    6001 (0.7%, 22.7%)    353 (1.1%, 17.0%)  miette::eyreish::error::object_drop
    6001 (0.7%, 23.4%)    353 (1.1%, 18.1%)  miette::eyreish::error::object_drop_front
    5648 (0.7%, 24.1%)    353 (1.1%, 19.1%)  <miette::eyreish::error::ErrorImpl<E> as core::fmt::Debug>::fmt
```

It's totalling more than 50k llvm lines, and is putting pressure on rustc codegen (the purple part on `oxc_linter` in the image above.

---

It's pretty obvious by looking at https://github.com/zkat/miette/blob/main/src/eyreish/error.rs, the generics can expand out to lots of code.
  • Loading branch information
Boshen committed May 11, 2024
1 parent 46c02ae commit 2064ae9
Show file tree
Hide file tree
Showing 34 changed files with 682 additions and 509 deletions.
106 changes: 103 additions & 3 deletions crates/oxc_diagnostics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ mod graphical_theme;
mod reporter;
mod service;

use std::path::PathBuf;
use std::{
fmt::{self, Display},
ops::Deref,
path::PathBuf,
};

pub use miette;
pub use thiserror;
Expand All @@ -21,9 +25,10 @@ pub type Error = miette::Error;
pub type Severity = miette::Severity;
pub type Report = miette::Report;

pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T> = std::result::Result<T, OxcDiagnostic>;

use miette::Diagnostic;
pub use miette::LabeledSpan;
use miette::{Diagnostic, SourceCode};
use thiserror::Error;

#[derive(Debug, Error, Diagnostic)]
Expand All @@ -35,3 +40,98 @@ pub struct MinifiedFileError(pub PathBuf);
#[error("Failed to open file {0:?} with error \"{1}\"")]
#[diagnostic(help("Failed to open file {0:?} with error \"{1}\""))]
pub struct FailedToOpenFileError(pub PathBuf, pub std::io::Error);

#[derive(Debug, Clone)]
pub struct OxcDiagnostic {
// `Box` the data to make `OxcDiagnostic` 8 bytes so that `Result` is small.
// This is required because rust does not performance return value optimization.
inner: Box<OxcDiagnosticInner>,
}

impl Deref for OxcDiagnostic {
type Target = Box<OxcDiagnosticInner>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}

#[derive(Debug, Clone)]
pub struct OxcDiagnosticInner {
pub message: String,
pub labels: Option<Vec<LabeledSpan>>,
pub help: Option<String>,
}

impl fmt::Display for OxcDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.message)
}
}

impl std::error::Error for OxcDiagnostic {}

impl Diagnostic for OxcDiagnostic {
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
}

fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.labels
.as_ref()
.map(|ls| ls.iter().cloned())
.map(Box::new)
.map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
}
}

impl OxcDiagnostic {
#[must_use]
pub fn new<T: Into<String>>(message: T) -> Self {
Self {
inner: Box::new(OxcDiagnosticInner {
message: message.into(),
labels: None,
help: None,
}),
}
}

#[must_use]
pub fn with_help<T: Into<String>>(mut self, help: T) -> Self {
self.inner.help = Some(help.into());
self
}

#[must_use]
pub fn with_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
self.inner.labels = Some(vec![label.into()]);
self
}

#[must_use]
pub fn with_labels<T: IntoIterator<Item = LabeledSpan>>(mut self, labels: T) -> Self {
self.inner.labels = Some(labels.into_iter().collect());
self
}

#[must_use]
pub fn and_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
let mut labels = self.inner.labels.unwrap_or_default();
labels.push(label.into());
self.inner.labels = Some(labels);
self
}

#[must_use]
pub fn and_labels<T: IntoIterator<Item = LabeledSpan>>(mut self, labels: T) -> Self {
let mut all_labels = self.inner.labels.unwrap_or_default();
all_labels.extend(labels);
self.inner.labels = Some(all_labels);
self
}

#[must_use]
pub fn with_source_code<T: SourceCode + Send + Sync + 'static>(self, code: T) -> Error {
Error::from(self).with_source_code(code)
}
}
5 changes: 4 additions & 1 deletion crates/oxc_language_server/src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,10 @@ impl IsolatedLintHandler {
let reports = ret
.errors
.into_iter()
.map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None })
.map(|diagnostic| ErrorReport {
error: Error::from(diagnostic),
fixed_content: None,
})
.collect();
return Some(Self::wrap_diagnostics(path, &original_source_text, reports, start));
};
Expand Down
31 changes: 14 additions & 17 deletions crates/oxc_linter/examples/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ use std::{env, path::Path};

use oxc_allocator::Allocator;
use oxc_ast::AstKind;
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::{SourceType, Span};
Expand Down Expand Up @@ -35,18 +32,18 @@ fn main() -> std::io::Result<()> {
let semantic_ret =
SemanticBuilder::new(&source_text, source_type).with_trivias(ret.trivias).build(program);

let mut errors: Vec<oxc_diagnostics::Error> = vec![];
let mut errors: Vec<OxcDiagnostic> = vec![];

for node in semantic_ret.semantic.nodes().iter() {
match node.kind() {
AstKind::DebuggerStatement(stmt) => {
errors.push(NoDebugger(stmt.span).into());
errors.push(no_debugger(stmt.span));
}
AstKind::ArrayPattern(array) if array.elements.is_empty() => {
errors.push(NoEmptyPattern("array", array.span).into());
errors.push(no_empty_pattern("array", array.span));
}
AstKind::ObjectPattern(object) if object.properties.is_empty() => {
errors.push(NoEmptyPattern("object", object.span).into());
errors.push(no_empty_pattern("object", object.span));
}
_ => {}
}
Expand All @@ -61,7 +58,7 @@ fn main() -> std::io::Result<()> {
Ok(())
}

fn print_errors(source_text: &str, errors: Vec<oxc_diagnostics::Error>) {
fn print_errors(source_text: &str, errors: Vec<OxcDiagnostic>) {
for error in errors {
let error = error.with_source_code(source_text.to_string());
println!("{error:?}");
Expand All @@ -75,10 +72,9 @@ fn print_errors(source_text: &str, errors: Vec<oxc_diagnostics::Error>) {
// 1 │ debugger;
// · ─────────
// ╰────
#[derive(Debug, Error, Diagnostic)]
#[error("`debugger` statement is not allowed")]
#[diagnostic(severity(warning))]
struct NoDebugger(#[label] pub Span);
fn no_debugger(span0: Span) -> OxcDiagnostic {
OxcDiagnostic::new("`debugger` statement is not allowed").with_labels([span0.into()])
}

// This prints:
//
Expand All @@ -88,7 +84,8 @@ struct NoDebugger(#[label] pub Span);
// · ─┬
// · ╰── Empty object binding pattern
// ╰────
#[derive(Debug, Error, Diagnostic)]
#[error("empty destructuring pattern is not allowed")]
#[diagnostic(severity(warning))]
struct NoEmptyPattern(&'static str, #[label("Empty {0} binding pattern")] pub Span);
fn no_empty_pattern(s0: &str, span1: Span) -> OxcDiagnostic {
OxcDiagnostic::new("empty destructuring pattern is not allowed").with_labels([
LabeledSpan::new_with_span(Some(format!("Empty {s0} binding pattern")), span1),
])
}
6 changes: 5 additions & 1 deletion crates/oxc_linter/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,11 @@ impl Runtime {
.parse();

if !ret.errors.is_empty() {
return ret.errors.into_iter().map(|err| Message::new(err, None)).collect();
return ret
.errors
.into_iter()
.map(|err| Message::new(Error::from(err), None))
.collect();
};

let program = allocator.alloc(ret.program);
Expand Down
8 changes: 3 additions & 5 deletions crates/oxc_parser/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl<'a> ParserImpl<'a> {
fn test_escaped_keyword(&mut self, kind: Kind) {
if self.cur_token().escaped() && kind.is_all_keyword() {
let span = self.cur_token().span();
self.error(diagnostics::EscapedKeyword(span));
self.error(diagnostics::escaped_keyword(span));
}
}

Expand Down Expand Up @@ -158,7 +158,7 @@ impl<'a> ParserImpl<'a> {
pub(crate) fn asi(&mut self) -> Result<()> {
if !self.can_insert_semicolon() {
let span = Span::new(self.prev_token_end, self.cur_token().start);
return Err(diagnostics::AutoSemicolonInsertion(span).into());
return Err(diagnostics::auto_semicolon_insertion(span));
}
if self.at(Kind::Semicolon) {
self.advance(Kind::Semicolon);
Expand All @@ -178,9 +178,7 @@ impl<'a> ParserImpl<'a> {
pub(crate) fn expect_without_advance(&mut self, kind: Kind) -> Result<()> {
if !self.at(kind) {
let range = self.cur_token().span();
return Err(
diagnostics::ExpectToken(kind.to_str(), self.cur_kind().to_str(), range).into()
);
return Err(diagnostics::expect_token(kind.to_str(), self.cur_kind().to_str(), range));
}
Ok(())
}
Expand Down

0 comments on commit 2064ae9

Please sign in to comment.