#[doc(hidden)]
pub use ecow::{EcoString, EcoVec, eco_format, eco_vec};
use std::backtrace::{Backtrace, BacktraceStatus};
use std::fmt::{self, Display, Formatter, Write as _};
use std::io;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use std::string::FromUtf8Error;
use az::SaturatingAs;
use comemo::Tracked;
use typst_syntax::package::{PackageSpec, PackageVersion};
use typst_syntax::{
DiagSpan, Lines, RealizeError, Span, Spanned, SyntaxDiagnostic, VirtualRoot,
};
use utf8_iter::ErrorReportingUtf8Chars;
use crate::engine::Engine;
use crate::loading::{LoadSource, Loaded};
use crate::{World, WorldExt};
#[macro_export]
#[doc(hidden)]
#[clippy::format_args]
macro_rules! __bail {
(
$fmt:literal $(, $arg:expr)* $(,)?
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(;)?
) => {
return Err($crate::diag::error!(
$fmt $(, $arg)*
$(; hint: $hint $(, $hint_arg)*)*
))
};
($error:expr) => {
return Err($crate::diag::eco_vec![$error])
};
($($tts:tt)*) => {
return Err($crate::diag::eco_vec![$crate::diag::error!($($tts)*)])
};
}
#[macro_export]
#[doc(hidden)]
#[clippy::format_args]
macro_rules! __error {
($fmt:literal $(, $arg:expr)* $(,)?) => {
$crate::diag::eco_format!($fmt $(, $arg)*).into()
};
(
$fmt:literal $(, $arg:expr)* $(,)?
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(;)?
) => {
$crate::diag::HintedString::new(
$crate::diag::eco_format!($fmt $(, $arg)*)
) $(.with_hint($crate::diag::eco_format!($hint $(, $hint_arg)*)))*
};
(
$span:expr, $fmt:literal $(, $arg:expr)* $(,)?
$(; hint $([$hint_span:expr])? : $hint:literal $(, $hint_arg:expr)*)*
$(;)?
) => {{
#[allow(unused_mut)]
let mut err = $crate::diag::SourceDiagnostic::error(
$span,
$crate::diag::eco_format!($fmt $(, $arg)*)
);
$($crate::diag::error!(hint$([$hint_span])?: err, $hint $(, $hint_arg)*);)*
err
}};
(hint: $err:ident, $hint:literal $(, $hint_arg:expr)*) => {
$err.hint($crate::diag::eco_format!($hint $(, $hint_arg)*))
};
(hint[$hint_span:expr]: $err:ident, $hint:literal $(, $hint_arg:expr)*) => {
$err.spanned_hint($crate::diag::eco_format!($hint $(, $hint_arg)*), $hint_span)
};
}
#[macro_export]
#[doc(hidden)]
#[clippy::format_args]
macro_rules! __warning {
(
$span:expr, $fmt:literal $(, $arg:expr)* $(,)?
$(; hint $([$hint_span:expr])? : $hint:literal $(, $hint_arg:expr)*)*
$(;)?
) => {{
#[allow(unused_mut)]
let mut warning = $crate::diag::SourceDiagnostic::warning(
$span,
$crate::diag::eco_format!($fmt $(, $arg)*)
);
$($crate::diag::error!(hint$([$hint_span])?: warning, $hint $(, $hint_arg)*);)*
warning
}};
}
#[rustfmt::skip]
#[doc(inline)]
pub use {
__bail as bail,
__error as error,
__warning as warning,
};
pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
pub trait CollectCombinedResult {
type Item;
fn collect_combined_result<B>(self) -> SourceResult<B>
where
B: FromIterator<Self::Item>;
}
impl<I, T> CollectCombinedResult for I
where
I: Iterator<Item = SourceResult<T>>,
{
type Item = T;
fn collect_combined_result<B>(self) -> SourceResult<B>
where
B: FromIterator<Self::Item>,
{
let mut errors = EcoVec::new();
let collected = self
.filter_map(|result| match result {
Ok(item) => Some(item),
Err(errs) => {
errors.extend(errs);
None
}
})
.collect();
if !errors.is_empty() {
return Err(errors);
}
Ok(collected)
}
}
pub trait ParallelCollectCombinedResult {
type Item;
fn collect_combined_result<B>(self) -> SourceResult<B>
where
B: FromIterator<Self::Item>;
}
impl<I, T> ParallelCollectCombinedResult for I
where
I: rayon::iter::ParallelIterator<Item = SourceResult<T>>,
T: Send,
{
type Item = T;
fn collect_combined_result<B>(self) -> SourceResult<B>
where
B: FromIterator<Self::Item>,
{
self.collect::<Vec<_>>().into_iter().collect_combined_result()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Warned<T> {
pub output: T,
pub warnings: EcoVec<SourceDiagnostic>,
}
impl<T> Warned<T> {
pub fn map<R, F: FnOnce(T) -> R>(self, f: F) -> Warned<R> {
Warned { output: f(self.output), warnings: self.warnings }
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct SourceDiagnostic {
pub severity: Severity,
pub span: DiagSpan,
pub message: EcoString,
pub trace: EcoVec<Spanned<Tracepoint>>,
pub hints: EcoVec<Spanned<EcoString, DiagSpan>>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Severity {
Error,
Warning,
}
impl SourceDiagnostic {
pub fn error(span: impl Into<DiagSpan>, message: impl Into<EcoString>) -> Self {
Self {
severity: Severity::Error,
span: span.into(),
trace: eco_vec![],
message: message.into(),
hints: eco_vec![],
}
}
pub fn warning(span: impl Into<DiagSpan>, message: impl Into<EcoString>) -> Self {
Self {
severity: Severity::Warning,
span: span.into(),
trace: eco_vec![],
message: message.into(),
hints: eco_vec![],
}
}
pub fn hint(&mut self, hint: impl Into<EcoString>) {
self.hints.push(Spanned::detached(hint.into()));
}
pub fn spanned_hint(
&mut self,
hint: impl Into<EcoString>,
span: impl Into<DiagSpan>,
) {
self.hints.push(Spanned::new(hint.into(), span.into()));
}
pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
self.hint(hint);
self
}
pub fn with_spanned_hint(
mut self,
hint: impl Into<EcoString>,
span: impl Into<DiagSpan>,
) -> Self {
self.spanned_hint(hint, span);
self
}
pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
self.hints.extend(hints.into_iter().map(Spanned::detached));
self
}
pub fn with_tracepoint(mut self, tracepoint: Tracepoint, span: Span) -> Self {
self.trace.push(Spanned::new(tracepoint, span));
self
}
}
impl From<SyntaxDiagnostic> for SourceDiagnostic {
fn from(syntax_diag: SyntaxDiagnostic) -> Self {
let SyntaxDiagnostic { is_error, span, message, hints } = syntax_diag;
Self {
severity: if is_error { Severity::Error } else { Severity::Warning },
span,
message,
trace: eco_vec![],
hints,
}
}
}
pub trait WarningSink {
fn emit(&mut self, message: HintedString);
}
impl WarningSink for () {
fn emit(&mut self, _: HintedString) {}
}
impl WarningSink for (&mut Engine<'_>, Span) {
fn emit(&mut self, hinted: HintedString) {
self.0.sink.warn(
SourceDiagnostic::warning(self.1, hinted.message())
.with_hints(hinted.hints().iter().cloned()),
);
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Tracepoint {
Call(Option<EcoString>),
Show(EcoString),
Import(EcoString),
Include(EcoString),
}
impl Display for Tracepoint {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Tracepoint::Call(Some(name)) => write!(f, "while calling `{name}`"),
Tracepoint::Call(None) => write!(f, "while calling function"),
Tracepoint::Show(name) => write!(f, "while showing {name} element"),
Tracepoint::Import(name) => write!(f, "while importing `{name}`"),
Tracepoint::Include(name) => write!(f, "while including `{name}`"),
}
}
}
pub trait Trace<T> {
fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint;
}
impl<T> Trace<T> for SourceResult<T> {
fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint,
{
self.map_err(|mut errors| {
let Some(trace_range) = world.range(span) else { return errors };
for error in errors.make_mut().iter_mut() {
if let Some(error_range) = world.range(error.span)
&& error.span.id() == span.id()
&& trace_range.start <= error_range.start
&& trace_range.end >= error_range.end
{
continue;
}
error.trace.push(Spanned::new(make_point(), span));
}
errors
})
}
}
pub type StrResult<T> = Result<T, EcoString>;
pub trait At<T> {
fn at(self, span: Span) -> SourceResult<T>;
}
impl<T, S> At<T> for Result<T, S>
where
S: Into<EcoString>,
{
fn at(self, span: Span) -> SourceResult<T> {
self.map_err(|message| eco_vec![SourceDiagnostic::error(span, message)])
}
}
pub type HintedStrResult<T> = Result<T, HintedString>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct HintedString(EcoVec<EcoString>);
impl HintedString {
pub fn new(message: EcoString) -> Self {
Self(eco_vec![message])
}
pub fn message(&self) -> &EcoString {
self.0.first().unwrap()
}
pub fn hints(&self) -> &[EcoString] {
self.0.get(1..).unwrap_or(&[])
}
pub fn hint(&mut self, hint: impl Into<EcoString>) {
self.0.push(hint.into());
}
pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
self.hint(hint);
self
}
pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
self.0.extend(hints);
self
}
}
impl<S> From<S> for HintedString
where
S: Into<EcoString>,
{
fn from(value: S) -> Self {
Self::new(value.into())
}
}
impl<T> At<T> for HintedStrResult<T> {
fn at(self, span: Span) -> SourceResult<T> {
self.map_err(|err| {
let mut components = err.0.into_iter();
let message = components.next().unwrap();
let diag = SourceDiagnostic::error(span, message).with_hints(components);
eco_vec![diag]
})
}
}
pub trait Hint<T> {
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
}
impl<T, S> Hint<T> for Result<T, S>
where
S: Into<EcoString>,
{
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
self.map_err(|message| HintedString::new(message.into()).with_hint(hint))
}
}
impl<T> Hint<T> for HintedStrResult<T> {
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
self.map_err(|mut error| {
error.hint(hint.into());
error
})
}
}
pub type FileResult<T> = Result<T, FileError>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum FileError {
NotFound(PathBuf),
AccessDenied,
IsDirectory,
NotSource,
InvalidUtf8,
Realize(RealizeError),
Package(PackageError),
Other(Option<EcoString>),
}
impl FileError {
pub fn from_io(err: io::Error, path: &Path) -> Self {
match err.kind() {
io::ErrorKind::NotFound => Self::NotFound(path.into()),
io::ErrorKind::PermissionDenied => Self::AccessDenied,
io::ErrorKind::InvalidData
if err.to_string().contains("stream did not contain valid UTF-8") =>
{
Self::InvalidUtf8
}
_ => Self::Other(Some(eco_format!("{err}"))),
}
}
}
impl std::error::Error for FileError {}
impl Display for FileError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::NotFound(path) => {
write!(f, "file not found (searched at {})", path.display())
}
Self::AccessDenied => f.pad("failed to load file (access denied)"),
Self::IsDirectory => f.pad("failed to load file (is a directory)"),
Self::NotSource => f.pad("not a Typst source file"),
Self::InvalidUtf8 => f.pad("file is not valid UTF-8"),
Self::Realize(err) => write!(f, "failed to load file ({err})"),
Self::Package(err) => err.fmt(f),
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
Self::Other(None) => f.pad("failed to load file"),
}
}
}
impl From<Utf8Error> for FileError {
fn from(_: Utf8Error) -> Self {
Self::InvalidUtf8
}
}
impl From<FromUtf8Error> for FileError {
fn from(_: FromUtf8Error) -> Self {
Self::InvalidUtf8
}
}
impl From<RealizeError> for FileError {
fn from(err: RealizeError) -> Self {
Self::Realize(err)
}
}
impl From<PackageError> for FileError {
fn from(err: PackageError) -> Self {
Self::Package(err)
}
}
impl From<FileError> for EcoString {
fn from(err: FileError) -> Self {
eco_format!("{err}")
}
}
pub type PackageResult<T> = Result<T, PackageError>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum PackageError {
NotFound(PackageSpec),
VersionNotFound(PackageSpec, PackageVersion),
NetworkFailed(Option<EcoString>),
MalformedArchive(Option<EcoString>),
Other(Option<EcoString>),
}
impl std::error::Error for PackageError {}
impl Display for PackageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::NotFound(spec) => {
write!(f, "package not found (searched for {spec})",)
}
Self::VersionNotFound(spec, latest) => {
write!(
f,
"package found, but version {} does not exist (latest is {})",
spec.version, latest,
)
}
Self::NetworkFailed(Some(err)) => {
write!(f, "failed to download package ({err})")
}
Self::NetworkFailed(None) => f.pad("failed to download package"),
Self::MalformedArchive(Some(err)) => {
write!(f, "failed to decompress package ({err})")
}
Self::MalformedArchive(None) => {
f.pad("failed to decompress package (archive malformed)")
}
Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
Self::Other(None) => f.pad("failed to load package"),
}
}
}
impl From<PackageError> for EcoString {
fn from(err: PackageError) -> Self {
eco_format!("{err}")
}
}
pub type LoadResult<T> = Result<T, LoadError>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LoadError {
text_pos: Option<ReportTextPos>,
message: EcoString,
}
impl LoadError {
pub fn text(
pos: impl Into<ReportTextPos>,
message: impl std::fmt::Display,
error: impl std::fmt::Display,
) -> Self {
Self {
text_pos: Some(pos.into()),
message: eco_format!("{message} ({error})"),
}
}
pub fn binary(
message: impl std::fmt::Display,
error: impl std::fmt::Display,
) -> Self {
Self {
text_pos: None,
message: eco_format!("{message} ({error})"),
}
}
}
impl From<Utf8Error> for LoadError {
fn from(err: Utf8Error) -> Self {
let start = err.valid_up_to();
let end = start + err.error_len().unwrap_or(0);
LoadError::text(
start..end,
"failed to convert to string",
"file is not valid UTF-8",
)
}
}
pub trait LoadedWithin {
type Output;
fn within(self, loaded: &Loaded) -> Self::Output;
}
impl<E> LoadedWithin for E
where
E: Into<LoadError>,
{
type Output = SourceDiagnostic;
fn within(self, loaded: &Loaded) -> Self::Output {
let LoadError { text_pos: pos, message } = self.into();
if let Some(pos) = pos {
load_err_in_text(loaded, pos, message)
} else {
load_err_in_binary(loaded, None, message)
}
}
}
impl<T, E> LoadedWithin for Result<T, E>
where
E: Into<LoadError>,
{
type Output = SourceResult<T>;
fn within(self, loaded: &Loaded) -> Self::Output {
self.map_err(|err| eco_vec![err.within(loaded)])
}
}
fn load_err_in_text(
loaded: &Loaded,
pos: ReportTextPos,
mut message: EcoString,
) -> SourceDiagnostic {
let Ok(lines) = loaded.data.lines() else {
return load_err_in_binary(loaded, Some(pos), message);
};
match loaded.source.v {
LoadSource::Path(file_id) => {
if let Some(range) = pos.range(&lines) {
let span = DiagSpan::from_range(file_id, range);
return SourceDiagnostic::error(span, message);
}
let span = DiagSpan::from_range(file_id, 0..loaded.data.len());
if let Some(pair) = pos.line_col(&lines) {
message.pop();
let (line, col) = pair.numbers();
write!(&mut message, " at {line}:{col})").ok();
}
SourceDiagnostic::error(span, message)
}
LoadSource::Bytes => {
if let Some(pair) = pos.line_col(&lines) {
message.pop();
let (line, col) = pair.numbers();
write!(&mut message, " at {line}:{col})").ok();
}
SourceDiagnostic::error(loaded.source.span, message)
}
}
}
fn load_err_in_binary(
loaded: &Loaded,
pos: Option<ReportTextPos>,
mut message: EcoString,
) -> SourceDiagnostic {
let line_col = pos
.and_then(|pos| pos.try_line_col(&loaded.data))
.map(|p| p.numbers());
match loaded.source.v {
LoadSource::Path(file) => {
message.pop();
match file.root() {
VirtualRoot::Project => {
write!(&mut message, " in {}", file.vpath().get_without_slash()).ok();
}
VirtualRoot::Package(package) => {
write!(
&mut message,
" in {package}{}",
file.vpath().get_with_slash()
)
.ok();
}
}
if let Some((line, col)) = line_col {
write!(&mut message, ":{line}:{col}").ok();
}
message.push(')');
}
LoadSource::Bytes => {
if let Some((line, col)) = line_col {
message.pop();
write!(&mut message, " at {line}:{col})").ok();
}
}
}
SourceDiagnostic::error(loaded.source.span, message)
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum ReportTextPos {
Full(std::ops::Range<u32>, LineCol),
Range(std::ops::Range<u32>),
LineCol(LineCol),
#[default]
None,
}
impl From<std::ops::Range<usize>> for ReportTextPos {
fn from(value: std::ops::Range<usize>) -> Self {
Self::Range(value.start.saturating_as()..value.end.saturating_as())
}
}
impl From<LineCol> for ReportTextPos {
fn from(value: LineCol) -> Self {
Self::LineCol(value)
}
}
impl ReportTextPos {
pub fn full(range: std::ops::Range<usize>, pair: LineCol) -> Self {
let range = range.start.saturating_as()..range.end.saturating_as();
Self::Full(range, pair)
}
fn range(&self, lines: &Lines<String>) -> Option<std::ops::Range<usize>> {
match self {
ReportTextPos::Full(range, _) => {
Some(range.start as usize..range.end as usize)
}
ReportTextPos::Range(range) => Some(range.start as usize..range.end as usize),
&ReportTextPos::LineCol(pair) => {
let i =
lines.line_column_to_byte(pair.line as usize, pair.col as usize)?;
Some(i..i)
}
ReportTextPos::None => None,
}
}
fn line_col(&self, lines: &Lines<String>) -> Option<LineCol> {
match self {
&ReportTextPos::Full(_, pair) => Some(pair),
ReportTextPos::Range(range) => {
let (line, col) = lines.byte_to_line_column(range.start as usize)?;
Some(LineCol::zero_based(line, col))
}
&ReportTextPos::LineCol(pair) => Some(pair),
ReportTextPos::None => None,
}
}
fn try_line_col(&self, bytes: &[u8]) -> Option<LineCol> {
match self {
&ReportTextPos::Full(_, pair) => Some(pair),
ReportTextPos::Range(range) => {
LineCol::try_from_byte_pos(range.start as usize, bytes)
}
&ReportTextPos::LineCol(pair) => Some(pair),
ReportTextPos::None => None,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct LineCol {
line: u32,
col: u32,
}
impl LineCol {
pub fn zero_based(line: usize, col: usize) -> Self {
Self {
line: line.saturating_as(),
col: col.saturating_as(),
}
}
pub fn one_based(line: usize, col: usize) -> Self {
Self::zero_based(line.saturating_sub(1), col.saturating_sub(1))
}
pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> {
let bytes = &bytes[..pos];
let mut line = 0;
#[allow(clippy::double_ended_iterator_last)]
let line_start = memchr::memchr_iter(b'\n', bytes)
.inspect(|_| line += 1)
.last()
.map(|i| i + 1)
.unwrap_or(bytes.len());
let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count();
Some(LineCol::zero_based(line, col))
}
pub fn indices(&self) -> (usize, usize) {
(self.line as usize, self.col as usize)
}
pub fn numbers(&self) -> (usize, usize) {
(self.line as usize + 1, self.col as usize + 1)
}
}
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError {
let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
let message = match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
eco_format!(
"failed to parse {format} (found closing tag '{actual}' instead of '{expected}')"
)
}
roxmltree::Error::UnknownEntityReference(entity, _) => {
eco_format!("failed to parse {format} (unknown entity '{entity}')")
}
roxmltree::Error::DuplicatedAttribute(attr, _) => {
eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
}
roxmltree::Error::NoRootNode => {
eco_format!("failed to parse {format} (missing root node)")
}
err => eco_format!("failed to parse {format} ({err})"),
};
LoadError { text_pos: Some(pos.into()), message }
}
#[track_caller]
pub fn assert_internal(cond: bool, msg: &str) -> HintedStrResult<()> {
if !cond { Err(internal_error(msg)) } else { Ok(()) }
}
#[track_caller]
pub fn panic_internal(msg: &str) -> HintedStrResult<()> {
Err(internal_error(msg))
}
pub trait ExpectInternal<T> {
fn expect_internal(self, msg: &str) -> HintedStrResult<T>;
}
impl<T> ExpectInternal<T> for Option<T> {
#[track_caller]
fn expect_internal(self, msg: &str) -> HintedStrResult<T> {
match self {
Some(val) => Ok(val),
None => Err(internal_error(msg)),
}
}
}
#[track_caller]
fn internal_error(msg: &str) -> HintedString {
let loc = std::panic::Location::caller();
let mut error = error!(
"internal error: {msg} (occurred at {loc})";
hint: "please report this as a bug";
);
if cfg!(debug_assertions) {
let backtrace = Backtrace::capture();
if backtrace.status() == BacktraceStatus::Captured {
error.hint(eco_format!("compiler backtrace:\n{backtrace}"));
} else {
error.hint("set `RUST_BACKTRACE` to `1` or `full` to capture a backtrace");
}
}
error
}