Skip to main content

typst_library/
diag.rs

1//! Diagnostics.
2
3// We re-export these types from `ecow` so that the macros below can write
4// `$crate::diag::eco_format` instead of `::ecow::eco_format`. This allows
5// downstream crates to use the macros without needing to include `ecow` as a
6// direct dependency of the crate.
7#[doc(hidden)]
8pub use ecow::{EcoString, EcoVec, eco_format, eco_vec};
9
10use std::backtrace::{Backtrace, BacktraceStatus};
11use std::fmt::{self, Display, Formatter, Write as _};
12use std::io;
13use std::path::{Path, PathBuf};
14use std::str::Utf8Error;
15use std::string::FromUtf8Error;
16
17use az::SaturatingAs;
18use comemo::Tracked;
19use typst_syntax::package::{PackageSpec, PackageVersion};
20use typst_syntax::{
21    DiagSpan, Lines, RealizeError, Span, Spanned, SyntaxDiagnostic, VirtualRoot,
22};
23use utf8_iter::ErrorReportingUtf8Chars;
24
25use crate::engine::Engine;
26use crate::loading::{LoadSource, Loaded};
27use crate::{World, WorldExt};
28
29/// Early-return with an error for common result types used in Typst. If you
30/// need to interact with the produced errors more, consider using `error!` or
31/// `warning!` instead.
32///
33/// The main usage is `bail!(span, "message with {}", "formatting")`, which will
34/// early-return an error for a [`SourceResult`]. If you leave out the span, it
35/// will return an error for a [`StrResult`] or [`HintedStrResult`] instead.
36///
37/// You can also add hints by separating the initial message with a semicolon
38/// and writing `hint: "..."`, see the example.
39///
40/// ```ignore
41/// bail!("returning a {} error with no span", "formatted"); // StrResult (no span)
42/// bail!(span, "returning a {} error", "formatted"); // SourceResult (has a span)
43/// bail!(
44///     span, "returning a {} error", "formatted";
45///     hint: "with multiple hints";
46///     hint[hint_span]: "hints can have custom spans and {}", "formatting";
47/// ); // SourceResult
48/// ```
49#[macro_export]
50#[doc(hidden)]
51#[clippy::format_args]
52// See the comment below for why this is `__bail` and not `bail`.
53macro_rules! __bail {
54    // If we don't have a span, forward to `error!` to create a `StrResult` or
55    // `HintedStrResult`.
56    (
57        $fmt:literal $(, $arg:expr)* $(,)?
58        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
59        $(;)?
60    ) => {
61        return Err($crate::diag::error!(
62            $fmt $(, $arg)*
63            $(; hint: $hint $(, $hint_arg)*)*
64        ))
65    };
66
67    // Just early return for a `SourceResult`: `bail!(some_error)`.
68    ($error:expr) => {
69        return Err($crate::diag::eco_vec![$error])
70    };
71
72    // For `bail(span, ...)`, we reuse `error!` and produce a `SourceResult`.
73    ($($tts:tt)*) => {
74        return Err($crate::diag::eco_vec![$crate::diag::error!($($tts)*)])
75    };
76}
77
78/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with
79/// severity `Error`.
80///
81/// If you just want to quickly return an error, consider the `bail!` macro.
82/// If you want to create a warning, use the `warning!` macro.
83///
84/// You can also add hints by separating the initial message with a semicolon
85/// and writing `hint: "..."`, see the example.
86///
87/// ```ignore
88/// error!("a {} error with no span", "formatted"); // EcoString, same as `eco_format!`
89/// error!(span, "an error with a {} message", "formatted"); // SourceDiagnostic
90/// error!(
91///     span, "an error with a {} message", "formatted";
92///     hint: "with multiple hints";
93///     hint[hint_span]: "hints can have custom spans and {}", "formatting";
94/// ); // SourceDiagnostic
95/// ```
96#[macro_export]
97#[doc(hidden)]
98#[clippy::format_args]
99// See the comment below for why this is `__error` and not `error`.
100macro_rules! __error {
101    // For `error!("just a {}", "string")`.
102    ($fmt:literal $(, $arg:expr)* $(,)?) => {
103        $crate::diag::eco_format!($fmt $(, $arg)*).into()
104    };
105
106    // For `error!("a hinted {}", "string"; hint: "some hint"; hint: "...")`
107    (
108        $fmt:literal $(, $arg:expr)* $(,)?
109        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
110        $(;)?
111    ) => {
112        $crate::diag::HintedString::new(
113            $crate::diag::eco_format!($fmt $(, $arg)*)
114        ) $(.with_hint($crate::diag::eco_format!($hint $(, $hint_arg)*)))*
115    };
116
117    // For `error!(span, ...)`
118    // Hints may include a span inside brackets: `hint[span_expr]: "msg"`.
119    (
120        $span:expr, $fmt:literal $(, $arg:expr)* $(,)?
121        $(; hint $([$hint_span:expr])? : $hint:literal $(, $hint_arg:expr)*)*
122        $(;)?
123    ) => {{
124        #[allow(unused_mut)]
125        let mut err = $crate::diag::SourceDiagnostic::error(
126            $span,
127            $crate::diag::eco_format!($fmt $(, $arg)*)
128        );
129        // We use a recursive macro for hints to allow for optional spans.
130        $($crate::diag::error!(hint$([$hint_span])?: err, $hint $(, $hint_arg)*);)*
131        err
132    }};
133
134    // Internal recursive macro for adding hints with/without spans. Note that
135    // recursive macros must generate full expressions, so we can't use
136    // `.with_hint()` or `.with_spanned_hint()`.
137    (hint: $err:ident, $hint:literal $(, $hint_arg:expr)*) => {
138        $err.hint($crate::diag::eco_format!($hint $(, $hint_arg)*))
139    };
140    (hint[$hint_span:expr]: $err:ident, $hint:literal $(, $hint_arg:expr)*) => {
141        $err.spanned_hint($crate::diag::eco_format!($hint $(, $hint_arg)*), $hint_span)
142    };
143}
144
145/// Construct a [`SourceDiagnostic`] with severity `Warning`. To use the warning
146/// you will need to add it to a sink, likely inside the [`Engine`], e.g.
147/// `engine.sink.warn(warning!(...))`.
148///
149/// If you want to return early or construct an error, consider the `bail!` or
150/// `error!` macros instead.
151///
152/// You can also add hints by separating the initial message with a semicolon
153/// and writing `hint: "..."`, see the example.
154///
155/// ```ignore
156/// warning!(span, "warning with a {} message", "formatted");
157/// warning!(
158///     span, "warning with a {} message", "formatted";
159///     hint: "with multiple hints";
160///     hint[hint_span]: "hints can have custom spans and {}", "formatting";
161/// );
162/// ```
163#[macro_export]
164#[doc(hidden)]
165#[clippy::format_args]
166// See the comment below for why this is `__warning` and not `warning`.
167macro_rules! __warning {
168    (
169        $span:expr, $fmt:literal $(, $arg:expr)* $(,)?
170        $(; hint $([$hint_span:expr])? : $hint:literal $(, $hint_arg:expr)*)*
171        $(;)?
172    ) => {{
173        #[allow(unused_mut)]
174        let mut warning = $crate::diag::SourceDiagnostic::warning(
175            $span,
176            $crate::diag::eco_format!($fmt $(, $arg)*)
177        );
178        // We use a recursive macro for hints to allow for optional spans.
179        $($crate::diag::error!(hint$([$hint_span])?: warning, $hint $(, $hint_arg)*);)*
180        warning
181    }};
182}
183
184// We want the `bail`, `error`, and `warning` macros and their documentation to
185// be scoped locally to this module and imported like normal items, including by
186// modules within this crate. However Rust only allows public macro_rules macros
187// to be exported at the root of the crate, and gives us no tools to avoid that.
188// See the "Import and Export" chapter of "The Little Book of Rust Macros" for
189// more: <https://lukaswirth.dev/tlborm/decl-macros/minutiae/import-export.html>
190//
191// Our solution is simple: the actual macros are named with two underscores, and
192// while they are available at the root of the crate, we add `doc(hidden)` to
193// hide their docs at the crate root. We then we re-export them here with new
194// names and `doc(inline)` so the preferred names and their documentation are
195// scoped to this module.
196//
197// Unfortunately, `__bail` is still available at the crate root here and in all
198// importers, but its name should suggest that we prefer to use `bail` instead.
199// Note that the `disallowed_macros` lint does not handle re-exports like this.
200#[rustfmt::skip]
201#[doc(inline)]
202pub use {
203    __bail as bail,
204    __error as error,
205    __warning as warning,
206};
207
208/// A result that can carry multiple source errors. The recommended way to
209/// create an error for this type is with the `bail!` macro.
210pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
211
212/// Collects an iterator of [`SourceResult`]s into a result containg a collection
213/// or the accumulated errors.
214///
215/// Unlike normal `FromIterator` for `Result`, this will combine all the errors.
216/// This is possible because a [`SourceResult`] can hold multiple errors.
217pub trait CollectCombinedResult {
218    type Item;
219
220    fn collect_combined_result<B>(self) -> SourceResult<B>
221    where
222        B: FromIterator<Self::Item>;
223}
224
225impl<I, T> CollectCombinedResult for I
226where
227    I: Iterator<Item = SourceResult<T>>,
228{
229    type Item = T;
230
231    fn collect_combined_result<B>(self) -> SourceResult<B>
232    where
233        B: FromIterator<Self::Item>,
234    {
235        let mut errors = EcoVec::new();
236        let collected = self
237            .filter_map(|result| match result {
238                Ok(item) => Some(item),
239                Err(errs) => {
240                    errors.extend(errs);
241                    None
242                }
243            })
244            .collect();
245        if !errors.is_empty() {
246            return Err(errors);
247        }
248        Ok(collected)
249    }
250}
251
252/// A variation of [`CollectCombinedResult`] for parallel rayon iterators.
253///
254/// Needs to be a separate trait because we can't have two blanket impls.
255pub trait ParallelCollectCombinedResult {
256    type Item;
257
258    fn collect_combined_result<B>(self) -> SourceResult<B>
259    where
260        B: FromIterator<Self::Item>;
261}
262
263impl<I, T> ParallelCollectCombinedResult for I
264where
265    I: rayon::iter::ParallelIterator<Item = SourceResult<T>>,
266    T: Send,
267{
268    type Item = T;
269
270    fn collect_combined_result<B>(self) -> SourceResult<B>
271    where
272        B: FromIterator<Self::Item>,
273    {
274        // A more efficient approach might be possible, but this is simpler and
275        // pragmatic. The point of this trait is primarily to make the call-site
276        // convenient, not to maximize efficiency.
277        self.collect::<Vec<_>>().into_iter().collect_combined_result()
278    }
279}
280
281/// An output alongside warnings generated while producing it.
282#[derive(Debug, Clone, Eq, PartialEq, Hash)]
283pub struct Warned<T> {
284    /// The produced output.
285    pub output: T,
286    /// Warnings generated while producing the output.
287    pub warnings: EcoVec<SourceDiagnostic>,
288}
289
290impl<T> Warned<T> {
291    /// Maps the output, keeping the same warnings.
292    pub fn map<R, F: FnOnce(T) -> R>(self, f: F) -> Warned<R> {
293        Warned { output: f(self.output), warnings: self.warnings }
294    }
295}
296
297/// An error or warning in a source or text file. The recommended way to create
298/// one is with the `error!` or `warning!` macros.
299///
300/// The contained spans will only be detached if any of the input source files
301/// were detached.
302#[derive(Debug, Clone, Eq, PartialEq, Hash)]
303pub struct SourceDiagnostic {
304    /// Whether the diagnostic is an error or a warning.
305    pub severity: Severity,
306    /// The span of a relevant node in a Typst source file or the relevant byte
307    /// range of an external file.
308    pub span: DiagSpan,
309    /// A diagnostic message describing the problem.
310    pub message: EcoString,
311    /// The trace of function calls leading to the problem.
312    pub trace: EcoVec<Spanned<Tracepoint>>,
313    /// Additional hints to the user.
314    ///
315    /// - When the span is `None`, these are generic hints. The CLI renders them
316    ///   as a list at the bottom, each prefixed with `hint: `.
317    ///
318    /// - When a span is given, the hint is related to a secondary piece of code
319    ///   and will be annotated at that code.
320    pub hints: EcoVec<Spanned<EcoString, DiagSpan>>,
321}
322
323/// The severity of a [`SourceDiagnostic`].
324#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
325pub enum Severity {
326    /// A fatal error.
327    Error,
328    /// A non-fatal warning.
329    Warning,
330}
331
332impl SourceDiagnostic {
333    /// Create a new, bare error.
334    pub fn error(span: impl Into<DiagSpan>, message: impl Into<EcoString>) -> Self {
335        Self {
336            severity: Severity::Error,
337            span: span.into(),
338            trace: eco_vec![],
339            message: message.into(),
340            hints: eco_vec![],
341        }
342    }
343
344    /// Create a new, bare warning.
345    pub fn warning(span: impl Into<DiagSpan>, message: impl Into<EcoString>) -> Self {
346        Self {
347            severity: Severity::Warning,
348            span: span.into(),
349            trace: eco_vec![],
350            message: message.into(),
351            hints: eco_vec![],
352        }
353    }
354
355    /// Adds a single hint to the diagnostic.
356    pub fn hint(&mut self, hint: impl Into<EcoString>) {
357        self.hints.push(Spanned::detached(hint.into()));
358    }
359
360    /// Adds a single hint specific to a source code location to the diagnostic.
361    pub fn spanned_hint(
362        &mut self,
363        hint: impl Into<EcoString>,
364        span: impl Into<DiagSpan>,
365    ) {
366        self.hints.push(Spanned::new(hint.into(), span.into()));
367    }
368
369    /// Adds a single hint to the diagnostic.
370    pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
371        self.hint(hint);
372        self
373    }
374
375    /// Adds a single hint specific to a source code location to the diagnostic.
376    pub fn with_spanned_hint(
377        mut self,
378        hint: impl Into<EcoString>,
379        span: impl Into<DiagSpan>,
380    ) -> Self {
381        self.spanned_hint(hint, span);
382        self
383    }
384
385    /// Adds multiple user-facing hints to the diagnostic.
386    pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
387        self.hints.extend(hints.into_iter().map(Spanned::detached));
388        self
389    }
390
391    /// Adds a single tracepoint to the diagnostic.
392    pub fn with_tracepoint(mut self, tracepoint: Tracepoint, span: Span) -> Self {
393        self.trace.push(Spanned::new(tracepoint, span));
394        self
395    }
396}
397
398impl From<SyntaxDiagnostic> for SourceDiagnostic {
399    fn from(syntax_diag: SyntaxDiagnostic) -> Self {
400        let SyntaxDiagnostic { is_error, span, message, hints } = syntax_diag;
401        Self {
402            severity: if is_error { Severity::Error } else { Severity::Warning },
403            span,
404            message,
405            trace: eco_vec![],
406            hints,
407        }
408    }
409}
410
411/// Destination for a warning message.
412pub trait WarningSink {
413    /// Emits the message as a warning.
414    fn emit(&mut self, message: HintedString);
415}
416
417impl WarningSink for () {
418    fn emit(&mut self, _: HintedString) {}
419}
420
421impl WarningSink for (&mut Engine<'_>, Span) {
422    fn emit(&mut self, hinted: HintedString) {
423        self.0.sink.warn(
424            SourceDiagnostic::warning(self.1, hinted.message())
425                .with_hints(hinted.hints().iter().cloned()),
426        );
427    }
428}
429
430/// A part of a diagnostic's [trace](SourceDiagnostic::trace).
431#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
432pub enum Tracepoint {
433    /// A function call.
434    Call(Option<EcoString>),
435    /// A show rule application.
436    Show(EcoString),
437    /// A module import.
438    Import(EcoString),
439    /// A module include.
440    Include(EcoString),
441}
442
443impl Display for Tracepoint {
444    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
445        match self {
446            Tracepoint::Call(Some(name)) => write!(f, "while calling `{name}`"),
447            Tracepoint::Call(None) => write!(f, "while calling function"),
448            Tracepoint::Show(name) => write!(f, "while showing {name} element"),
449            Tracepoint::Import(name) => write!(f, "while importing `{name}`"),
450            Tracepoint::Include(name) => write!(f, "while including `{name}`"),
451        }
452    }
453}
454
455/// Enrich a [`SourceResult`] with a tracepoint.
456pub trait Trace<T> {
457    /// Add the tracepoint to all errors that lie outside the `span`.
458    fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
459    where
460        F: Fn() -> Tracepoint;
461}
462
463impl<T> Trace<T> for SourceResult<T> {
464    fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
465    where
466        F: Fn() -> Tracepoint,
467    {
468        self.map_err(|mut errors| {
469            let Some(trace_range) = world.range(span) else { return errors };
470            for error in errors.make_mut().iter_mut() {
471                // Skip traces that surround the error.
472                if let Some(error_range) = world.range(error.span)
473                    && error.span.id() == span.id()
474                    && trace_range.start <= error_range.start
475                    && trace_range.end >= error_range.end
476                {
477                    continue;
478                }
479
480                error.trace.push(Spanned::new(make_point(), span));
481            }
482            errors
483        })
484    }
485}
486
487/// A result type with a string error message. The recommended way to create an
488/// error for this type is with the [`bail!`] macro.
489pub type StrResult<T> = Result<T, EcoString>;
490
491/// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by
492/// adding span information.
493pub trait At<T> {
494    /// Add the span information.
495    fn at(self, span: Span) -> SourceResult<T>;
496}
497
498impl<T, S> At<T> for Result<T, S>
499where
500    S: Into<EcoString>,
501{
502    fn at(self, span: Span) -> SourceResult<T> {
503        self.map_err(|message| eco_vec![SourceDiagnostic::error(span, message)])
504    }
505}
506
507/// A result type with a string error message and hints. The recommended way to
508/// create an error for this type is with the `bail!` macro.
509pub type HintedStrResult<T> = Result<T, HintedString>;
510
511/// A string message with hints. The recommended way to create one is with the
512/// `error!` macro.
513///
514/// This is internally represented by a vector of strings.
515/// - The first element of the vector contains the message.
516/// - The remaining elements are the hints.
517/// - This is done to reduce the size of a HintedString.
518/// - The vector is guaranteed to not be empty.
519#[derive(Debug, Clone, Eq, PartialEq, Hash)]
520pub struct HintedString(EcoVec<EcoString>);
521
522impl HintedString {
523    /// Creates a new hinted string with the given message.
524    pub fn new(message: EcoString) -> Self {
525        Self(eco_vec![message])
526    }
527
528    /// A diagnostic message describing the problem.
529    pub fn message(&self) -> &EcoString {
530        self.0.first().unwrap()
531    }
532
533    /// Additional hints to the user, indicating how this error could be avoided
534    /// or worked around.
535    pub fn hints(&self) -> &[EcoString] {
536        self.0.get(1..).unwrap_or(&[])
537    }
538
539    /// Adds a single hint to the hinted string.
540    pub fn hint(&mut self, hint: impl Into<EcoString>) {
541        self.0.push(hint.into());
542    }
543
544    /// Adds a single hint to the hinted string.
545    pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
546        self.hint(hint);
547        self
548    }
549
550    /// Adds user-facing hints to the hinted string.
551    pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
552        self.0.extend(hints);
553        self
554    }
555}
556
557impl<S> From<S> for HintedString
558where
559    S: Into<EcoString>,
560{
561    fn from(value: S) -> Self {
562        Self::new(value.into())
563    }
564}
565
566impl<T> At<T> for HintedStrResult<T> {
567    fn at(self, span: Span) -> SourceResult<T> {
568        self.map_err(|err| {
569            let mut components = err.0.into_iter();
570            let message = components.next().unwrap();
571            let diag = SourceDiagnostic::error(span, message).with_hints(components);
572            eco_vec![diag]
573        })
574    }
575}
576
577/// Enrich a [`StrResult`] or [`HintedStrResult`] with a hint.
578pub trait Hint<T> {
579    /// Add the hint.
580    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
581}
582
583impl<T, S> Hint<T> for Result<T, S>
584where
585    S: Into<EcoString>,
586{
587    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
588        self.map_err(|message| HintedString::new(message.into()).with_hint(hint))
589    }
590}
591
592impl<T> Hint<T> for HintedStrResult<T> {
593    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
594        self.map_err(|mut error| {
595            error.hint(hint.into());
596            error
597        })
598    }
599}
600
601/// A result type with a file-related error.
602pub type FileResult<T> = Result<T, FileError>;
603
604/// An error that occurred while trying to load a file.
605#[derive(Debug, Clone, Eq, PartialEq, Hash)]
606pub enum FileError {
607    /// A file was not found at this path.
608    NotFound(PathBuf),
609    /// A file could not be accessed.
610    AccessDenied,
611    /// A directory was found, but a file was expected.
612    IsDirectory,
613    /// The file is not a Typst source file, but should have been.
614    NotSource,
615    /// The file was not valid UTF-8, but should have been.
616    InvalidUtf8,
617    /// A virtual Typst path could not be realized into a real path on the
618    /// current platform.
619    Realize(RealizeError),
620    /// The package the file is part of could not be loaded.
621    Package(PackageError),
622    /// Another error.
623    ///
624    /// The optional string can give more details, if available.
625    Other(Option<EcoString>),
626}
627
628impl FileError {
629    /// Create a file error from an I/O error.
630    pub fn from_io(err: io::Error, path: &Path) -> Self {
631        match err.kind() {
632            io::ErrorKind::NotFound => Self::NotFound(path.into()),
633            io::ErrorKind::PermissionDenied => Self::AccessDenied,
634            io::ErrorKind::InvalidData
635                if err.to_string().contains("stream did not contain valid UTF-8") =>
636            {
637                Self::InvalidUtf8
638            }
639            _ => Self::Other(Some(eco_format!("{err}"))),
640        }
641    }
642}
643
644impl std::error::Error for FileError {}
645
646impl Display for FileError {
647    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
648        match self {
649            Self::NotFound(path) => {
650                write!(f, "file not found (searched at {})", path.display())
651            }
652            Self::AccessDenied => f.pad("failed to load file (access denied)"),
653            Self::IsDirectory => f.pad("failed to load file (is a directory)"),
654            Self::NotSource => f.pad("not a Typst source file"),
655            Self::InvalidUtf8 => f.pad("file is not valid UTF-8"),
656            Self::Realize(err) => write!(f, "failed to load file ({err})"),
657            Self::Package(err) => err.fmt(f),
658            Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
659            Self::Other(None) => f.pad("failed to load file"),
660        }
661    }
662}
663
664impl From<Utf8Error> for FileError {
665    fn from(_: Utf8Error) -> Self {
666        Self::InvalidUtf8
667    }
668}
669
670impl From<FromUtf8Error> for FileError {
671    fn from(_: FromUtf8Error) -> Self {
672        Self::InvalidUtf8
673    }
674}
675
676impl From<RealizeError> for FileError {
677    fn from(err: RealizeError) -> Self {
678        Self::Realize(err)
679    }
680}
681
682impl From<PackageError> for FileError {
683    fn from(err: PackageError) -> Self {
684        Self::Package(err)
685    }
686}
687
688impl From<FileError> for EcoString {
689    fn from(err: FileError) -> Self {
690        eco_format!("{err}")
691    }
692}
693
694/// A result type with a package-related error.
695pub type PackageResult<T> = Result<T, PackageError>;
696
697/// An error that occurred while trying to load a package.
698///
699/// Some variants have an optional string can give more details, if available.
700#[derive(Debug, Clone, Eq, PartialEq, Hash)]
701pub enum PackageError {
702    /// The specified package does not exist.
703    NotFound(PackageSpec),
704    /// The specified package was found, but the version does not exist.
705    VersionNotFound(PackageSpec, PackageVersion),
706    /// Failed to retrieve the package through the network.
707    NetworkFailed(Option<EcoString>),
708    /// The package archive was malformed.
709    MalformedArchive(Option<EcoString>),
710    /// Another error.
711    Other(Option<EcoString>),
712}
713
714impl std::error::Error for PackageError {}
715
716impl Display for PackageError {
717    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
718        match self {
719            Self::NotFound(spec) => {
720                write!(f, "package not found (searched for {spec})",)
721            }
722            Self::VersionNotFound(spec, latest) => {
723                write!(
724                    f,
725                    "package found, but version {} does not exist (latest is {})",
726                    spec.version, latest,
727                )
728            }
729            Self::NetworkFailed(Some(err)) => {
730                write!(f, "failed to download package ({err})")
731            }
732            Self::NetworkFailed(None) => f.pad("failed to download package"),
733            Self::MalformedArchive(Some(err)) => {
734                write!(f, "failed to decompress package ({err})")
735            }
736            Self::MalformedArchive(None) => {
737                f.pad("failed to decompress package (archive malformed)")
738            }
739            Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
740            Self::Other(None) => f.pad("failed to load package"),
741        }
742    }
743}
744
745impl From<PackageError> for EcoString {
746    fn from(err: PackageError) -> Self {
747        eco_format!("{err}")
748    }
749}
750
751/// A result type with a data-loading-related error.
752pub type LoadResult<T> = Result<T, LoadError>;
753
754/// A call site independent error that occurred during data loading. This avoids
755/// polluting the memoization with [`Span`]s and [`FileId`]s from source files.
756/// Can be turned into a [`SourceDiagnostic`] using the [`LoadedWithin::within`]
757/// method available on [`LoadResult`].
758///
759/// [`FileId`]: typst_syntax::FileId
760#[derive(Debug, Clone, Eq, PartialEq, Hash)]
761pub struct LoadError {
762    /// The position in the file at which the error occurred, or `None` for
763    /// binary sources.
764    text_pos: Option<ReportTextPos>,
765    /// Must contain a message formatted like this: `"failed to do thing (cause)"`.
766    message: EcoString,
767}
768
769impl LoadError {
770    /// Creates a new error from a position in a text file, a base message (e.g.
771    /// `failed to parse JSON`) and a concrete error (e.g. `invalid number`)
772    pub fn text(
773        pos: impl Into<ReportTextPos>,
774        message: impl std::fmt::Display,
775        error: impl std::fmt::Display,
776    ) -> Self {
777        Self {
778            text_pos: Some(pos.into()),
779            message: eco_format!("{message} ({error})"),
780        }
781    }
782
783    /// Creates a new error from a base message (e.g. `failed to parse PDF`) and
784    /// a concrete error (e.g. `invalid number`). For use with binary sources,
785    /// which do not have useful position information.
786    pub fn binary(
787        message: impl std::fmt::Display,
788        error: impl std::fmt::Display,
789    ) -> Self {
790        Self {
791            text_pos: None,
792            message: eco_format!("{message} ({error})"),
793        }
794    }
795}
796
797impl From<Utf8Error> for LoadError {
798    fn from(err: Utf8Error) -> Self {
799        let start = err.valid_up_to();
800        let end = start + err.error_len().unwrap_or(0);
801        LoadError::text(
802            start..end,
803            "failed to convert to string",
804            "file is not valid UTF-8",
805        )
806    }
807}
808
809/// Convert a [`LoadError`] or compatible [`Result`] to a [`SourceDiagnostic`]
810/// or [`SourceResult`] by adding the [`Loaded`] context.
811pub trait LoadedWithin {
812    /// The enriched type that has the context factored in.
813    type Output;
814
815    /// Report an error, possibly in an external file.
816    fn within(self, loaded: &Loaded) -> Self::Output;
817}
818
819impl<E> LoadedWithin for E
820where
821    E: Into<LoadError>,
822{
823    type Output = SourceDiagnostic;
824
825    fn within(self, loaded: &Loaded) -> Self::Output {
826        let LoadError { text_pos: pos, message } = self.into();
827        if let Some(pos) = pos {
828            load_err_in_text(loaded, pos, message)
829        } else {
830            load_err_in_binary(loaded, None, message)
831        }
832    }
833}
834
835impl<T, E> LoadedWithin for Result<T, E>
836where
837    E: Into<LoadError>,
838{
839    type Output = SourceResult<T>;
840
841    fn within(self, loaded: &Loaded) -> Self::Output {
842        self.map_err(|err| eco_vec![err.within(loaded)])
843    }
844}
845
846/// Report an error, possibly in an external file. This will delegate to
847/// [`load_err_in_binary`] if the data isn't valid UTF-8.
848fn load_err_in_text(
849    loaded: &Loaded,
850    pos: ReportTextPos,
851    mut message: EcoString,
852) -> SourceDiagnostic {
853    // This also does UTF-8 validation. Only report an error in an external
854    // file if it is human readable (valid UTF-8), otherwise fall back to
855    // `load_err_in_binary`.
856    let Ok(lines) = loaded.data.lines() else {
857        return load_err_in_binary(loaded, Some(pos), message);
858    };
859    match loaded.source.v {
860        LoadSource::Path(file_id) => {
861            if let Some(range) = pos.range(&lines) {
862                let span = DiagSpan::from_range(file_id, range);
863                return SourceDiagnostic::error(span, message);
864            }
865
866            // Either `ReportPos::None` was provided, or resolving the range
867            // from the line/column failed. If present report the possibly
868            // wrong line/column in the error message anyway.
869            let span = DiagSpan::from_range(file_id, 0..loaded.data.len());
870            if let Some(pair) = pos.line_col(&lines) {
871                message.pop();
872                let (line, col) = pair.numbers();
873                write!(&mut message, " at {line}:{col})").ok();
874            }
875            SourceDiagnostic::error(span, message)
876        }
877        LoadSource::Bytes => {
878            if let Some(pair) = pos.line_col(&lines) {
879                message.pop();
880                let (line, col) = pair.numbers();
881                write!(&mut message, " at {line}:{col})").ok();
882            }
883            SourceDiagnostic::error(loaded.source.span, message)
884        }
885    }
886}
887
888/// Report an error (possibly from an external file) that isn't valid UTF-8.
889fn load_err_in_binary(
890    loaded: &Loaded,
891    pos: Option<ReportTextPos>,
892    mut message: EcoString,
893) -> SourceDiagnostic {
894    let line_col = pos
895        .and_then(|pos| pos.try_line_col(&loaded.data))
896        .map(|p| p.numbers());
897    match loaded.source.v {
898        LoadSource::Path(file) => {
899            message.pop();
900            match file.root() {
901                VirtualRoot::Project => {
902                    write!(&mut message, " in {}", file.vpath().get_without_slash()).ok();
903                }
904                VirtualRoot::Package(package) => {
905                    write!(
906                        &mut message,
907                        " in {package}{}",
908                        file.vpath().get_with_slash()
909                    )
910                    .ok();
911                }
912            }
913            if let Some((line, col)) = line_col {
914                write!(&mut message, ":{line}:{col}").ok();
915            }
916            message.push(')');
917        }
918        LoadSource::Bytes => {
919            if let Some((line, col)) = line_col {
920                message.pop();
921                write!(&mut message, " at {line}:{col})").ok();
922            }
923        }
924    }
925    SourceDiagnostic::error(loaded.source.span, message)
926}
927
928/// A position in a text document at which an error was reported.
929#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
930pub enum ReportTextPos {
931    /// Contains a range, and a line/column pair.
932    Full(std::ops::Range<u32>, LineCol),
933    /// Contains a range.
934    Range(std::ops::Range<u32>),
935    /// Contains a line/column pair.
936    LineCol(LineCol),
937    #[default]
938    None,
939}
940
941impl From<std::ops::Range<usize>> for ReportTextPos {
942    fn from(value: std::ops::Range<usize>) -> Self {
943        Self::Range(value.start.saturating_as()..value.end.saturating_as())
944    }
945}
946
947impl From<LineCol> for ReportTextPos {
948    fn from(value: LineCol) -> Self {
949        Self::LineCol(value)
950    }
951}
952
953impl ReportTextPos {
954    /// Creates a position from a pre-existing range and line-column pair.
955    pub fn full(range: std::ops::Range<usize>, pair: LineCol) -> Self {
956        let range = range.start.saturating_as()..range.end.saturating_as();
957        Self::Full(range, pair)
958    }
959
960    /// Tries to determine the byte range for this position.
961    fn range(&self, lines: &Lines<String>) -> Option<std::ops::Range<usize>> {
962        match self {
963            ReportTextPos::Full(range, _) => {
964                Some(range.start as usize..range.end as usize)
965            }
966            ReportTextPos::Range(range) => Some(range.start as usize..range.end as usize),
967            &ReportTextPos::LineCol(pair) => {
968                let i =
969                    lines.line_column_to_byte(pair.line as usize, pair.col as usize)?;
970                Some(i..i)
971            }
972            ReportTextPos::None => None,
973        }
974    }
975
976    /// Tries to determine the line/column for this position.
977    fn line_col(&self, lines: &Lines<String>) -> Option<LineCol> {
978        match self {
979            &ReportTextPos::Full(_, pair) => Some(pair),
980            ReportTextPos::Range(range) => {
981                let (line, col) = lines.byte_to_line_column(range.start as usize)?;
982                Some(LineCol::zero_based(line, col))
983            }
984            &ReportTextPos::LineCol(pair) => Some(pair),
985            ReportTextPos::None => None,
986        }
987    }
988
989    /// Either gets the line/column pair, or tries to compute it from possibly
990    /// invalid UTF-8 data.
991    fn try_line_col(&self, bytes: &[u8]) -> Option<LineCol> {
992        match self {
993            &ReportTextPos::Full(_, pair) => Some(pair),
994            ReportTextPos::Range(range) => {
995                LineCol::try_from_byte_pos(range.start as usize, bytes)
996            }
997            &ReportTextPos::LineCol(pair) => Some(pair),
998            ReportTextPos::None => None,
999        }
1000    }
1001}
1002
1003/// A line/column pair.
1004#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1005pub struct LineCol {
1006    /// The 0-based line.
1007    line: u32,
1008    /// The 0-based column.
1009    col: u32,
1010}
1011
1012impl LineCol {
1013    /// Constructs the line/column pair from 0-based indices.
1014    pub fn zero_based(line: usize, col: usize) -> Self {
1015        Self {
1016            line: line.saturating_as(),
1017            col: col.saturating_as(),
1018        }
1019    }
1020
1021    /// Constructs the line/column pair from 1-based numbers.
1022    pub fn one_based(line: usize, col: usize) -> Self {
1023        Self::zero_based(line.saturating_sub(1), col.saturating_sub(1))
1024    }
1025
1026    /// Try to compute a line/column pair from possibly invalid UTF-8 data.
1027    pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> {
1028        let bytes = &bytes[..pos];
1029        let mut line = 0;
1030        #[allow(clippy::double_ended_iterator_last)]
1031        let line_start = memchr::memchr_iter(b'\n', bytes)
1032            .inspect(|_| line += 1)
1033            .last()
1034            .map(|i| i + 1)
1035            .unwrap_or(bytes.len());
1036
1037        let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count();
1038        Some(LineCol::zero_based(line, col))
1039    }
1040
1041    /// Returns the 0-based line/column indices.
1042    pub fn indices(&self) -> (usize, usize) {
1043        (self.line as usize, self.col as usize)
1044    }
1045
1046    /// Returns the 1-based line/column numbers.
1047    pub fn numbers(&self) -> (usize, usize) {
1048        (self.line as usize + 1, self.col as usize + 1)
1049    }
1050}
1051
1052/// Format a user-facing error message for an XML-like file format.
1053pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError {
1054    let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
1055    let message = match error {
1056        roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
1057            eco_format!(
1058                "failed to parse {format} (found closing tag '{actual}' instead of '{expected}')"
1059            )
1060        }
1061        roxmltree::Error::UnknownEntityReference(entity, _) => {
1062            eco_format!("failed to parse {format} (unknown entity '{entity}')")
1063        }
1064        roxmltree::Error::DuplicatedAttribute(attr, _) => {
1065            eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
1066        }
1067        roxmltree::Error::NoRootNode => {
1068            eco_format!("failed to parse {format} (missing root node)")
1069        }
1070        err => eco_format!("failed to parse {format} ({err})"),
1071    };
1072
1073    LoadError { text_pos: Some(pos.into()), message }
1074}
1075
1076/// Asserts a condition, generating an internal compiler error with the provided
1077/// message on failure.
1078#[track_caller]
1079pub fn assert_internal(cond: bool, msg: &str) -> HintedStrResult<()> {
1080    if !cond { Err(internal_error(msg)) } else { Ok(()) }
1081}
1082
1083/// Generates an internal compiler error with the provided message.
1084#[track_caller]
1085pub fn panic_internal(msg: &str) -> HintedStrResult<()> {
1086    Err(internal_error(msg))
1087}
1088
1089/// Adds a method analogous to [`Option::expect`] that raises an internal
1090/// compiler error instead of panicking.
1091pub trait ExpectInternal<T> {
1092    /// Extracts the value, producing an internal error if `self` is `None`.
1093    fn expect_internal(self, msg: &str) -> HintedStrResult<T>;
1094}
1095
1096impl<T> ExpectInternal<T> for Option<T> {
1097    #[track_caller]
1098    fn expect_internal(self, msg: &str) -> HintedStrResult<T> {
1099        match self {
1100            Some(val) => Ok(val),
1101            None => Err(internal_error(msg)),
1102        }
1103    }
1104}
1105
1106/// The shared internal implementation of [`assert_internal`],
1107/// [`panic_internal`] and [`ExpectInternal::expect_internal`].
1108#[track_caller]
1109fn internal_error(msg: &str) -> HintedString {
1110    let loc = std::panic::Location::caller();
1111    let mut error = error!(
1112        "internal error: {msg} (occurred at {loc})";
1113        hint: "please report this as a bug";
1114    );
1115
1116    if cfg!(debug_assertions) {
1117        let backtrace = Backtrace::capture();
1118        if backtrace.status() == BacktraceStatus::Captured {
1119            error.hint(eco_format!("compiler backtrace:\n{backtrace}"));
1120        } else {
1121            error.hint("set `RUST_BACKTRACE` to `1` or `full` to capture a backtrace");
1122        }
1123    }
1124
1125    error
1126}