typst_library/
diag.rs

1//! Diagnostics.
2
3use std::backtrace::{Backtrace, BacktraceStatus};
4use std::fmt::{self, Display, Formatter, Write as _};
5use std::io;
6use std::path::{Path, PathBuf};
7use std::str::Utf8Error;
8use std::string::FromUtf8Error;
9
10use az::SaturatingAs;
11use comemo::Tracked;
12use ecow::{EcoVec, eco_vec};
13use typst_syntax::package::{PackageSpec, PackageVersion};
14use typst_syntax::{Lines, Span, Spanned, SyntaxError};
15use utf8_iter::ErrorReportingUtf8Chars;
16
17use crate::engine::Engine;
18use crate::loading::{LoadSource, Loaded};
19use crate::{World, WorldExt};
20
21/// Early-return with a [`StrResult`] or [`SourceResult`].
22///
23/// If called with just a string and format args, returns with a
24/// `StrResult`. If called with a span, a string and format args, returns
25/// a `SourceResult`.
26///
27/// You can also emit hints with the `; hint: "..."` syntax.
28///
29/// ```ignore
30/// bail!("bailing with a {}", "string result");
31/// bail!(span, "bailing with a {}", "source result");
32/// bail!(
33///     span, "bailing with a {}", "source result";
34///     hint: "hint 1"
35/// );
36/// bail!(
37///     span, "bailing with a {}", "source result";
38///     hint: "hint 1";
39///     hint: "hint 2";
40/// );
41/// ```
42#[macro_export]
43#[doc(hidden)]
44macro_rules! __bail {
45    // For bail!("just a {}", "string")
46    (
47        $fmt:literal $(, $arg:expr)*
48        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
49        $(,)?
50    ) => {
51        return Err($crate::diag::error!(
52            $fmt $(, $arg)*
53            $(; hint: $hint $(, $hint_arg)*)*
54        ))
55    };
56
57    // For bail!(error!(..))
58    ($error:expr) => {
59        return Err(::ecow::eco_vec![$error])
60    };
61
62    // For bail(span, ...)
63    ($($tts:tt)*) => {
64        return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
65    };
66}
67
68/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with
69/// severity `Error`.
70#[macro_export]
71#[doc(hidden)]
72macro_rules! __error {
73    // For bail!("just a {}", "string").
74    ($fmt:literal $(, $arg:expr)* $(,)?) => {
75        $crate::diag::eco_format!($fmt, $($arg),*).into()
76    };
77
78    // For bail!("a hinted {}", "string"; hint: "some hint"; hint: "...")
79    (
80        $fmt:literal $(, $arg:expr)*
81        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
82        $(,)?
83    ) => {
84        $crate::diag::HintedString::new(
85            $crate::diag::eco_format!($fmt, $($arg),*)
86        ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
87    };
88
89    // For bail!(span, ...)
90    (
91        $span:expr, $fmt:literal $(, $arg:expr)*
92        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
93        $(,)?
94    ) => {
95        $crate::diag::SourceDiagnostic::error(
96            $span,
97            $crate::diag::eco_format!($fmt, $($arg),*),
98        )  $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
99    };
100}
101
102/// Construct a [`SourceDiagnostic`] with severity `Warning`.
103///
104/// You can also emit hints with the `; hint: "..."` syntax.
105///
106/// ```ignore
107/// warning!(span, "warning with a {}", "source result");
108/// warning!(
109///     span, "warning with a {}", "source result";
110///     hint: "hint 1"
111/// );
112/// warning!(
113///     span, "warning with a {}", "source result";
114///     hint: "hint 1";
115///     hint: "hint 2";
116/// );
117/// ```
118#[macro_export]
119#[doc(hidden)]
120macro_rules! __warning {
121    (
122        $span:expr,
123        $fmt:literal $(, $arg:expr)*
124        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
125        $(,)?
126    ) => {
127        $crate::diag::SourceDiagnostic::warning(
128            $span,
129            $crate::diag::eco_format!($fmt, $($arg),*),
130        ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
131    };
132}
133
134#[rustfmt::skip]
135#[doc(inline)]
136pub use {
137    crate::__bail as bail,
138    crate::__error as error,
139    crate::__warning as warning,
140    ecow::{eco_format, EcoString},
141};
142
143/// A result that can carry multiple source errors.
144pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
145
146/// An output alongside warnings generated while producing it.
147#[derive(Debug, Clone, Eq, PartialEq, Hash)]
148pub struct Warned<T> {
149    /// The produced output.
150    pub output: T,
151    /// Warnings generated while producing the output.
152    pub warnings: EcoVec<SourceDiagnostic>,
153}
154
155impl<T> Warned<T> {
156    /// Maps the output, keeping the same warnings.
157    pub fn map<R, F: FnOnce(T) -> R>(self, f: F) -> Warned<R> {
158        Warned { output: f(self.output), warnings: self.warnings }
159    }
160}
161
162/// An error or warning in a source or text file.
163///
164/// The contained spans will only be detached if any of the input source files
165/// were detached.
166#[derive(Debug, Clone, Eq, PartialEq, Hash)]
167pub struct SourceDiagnostic {
168    /// Whether the diagnostic is an error or a warning.
169    pub severity: Severity,
170    /// The span of the relevant node in the source code.
171    pub span: Span,
172    /// A diagnostic message describing the problem.
173    pub message: EcoString,
174    /// The trace of function calls leading to the problem.
175    pub trace: EcoVec<Spanned<Tracepoint>>,
176    /// Additional hints to the user, indicating how this problem could be avoided
177    /// or worked around.
178    pub hints: EcoVec<EcoString>,
179}
180
181/// The severity of a [`SourceDiagnostic`].
182#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
183pub enum Severity {
184    /// A fatal error.
185    Error,
186    /// A non-fatal warning.
187    Warning,
188}
189
190impl SourceDiagnostic {
191    /// Create a new, bare error.
192    pub fn error(span: Span, message: impl Into<EcoString>) -> Self {
193        Self {
194            severity: Severity::Error,
195            span,
196            trace: eco_vec![],
197            message: message.into(),
198            hints: eco_vec![],
199        }
200    }
201
202    /// Create a new, bare warning.
203    pub fn warning(span: Span, message: impl Into<EcoString>) -> Self {
204        Self {
205            severity: Severity::Warning,
206            span,
207            trace: eco_vec![],
208            message: message.into(),
209            hints: eco_vec![],
210        }
211    }
212
213    /// Adds a single hint to the diagnostic.
214    pub fn hint(&mut self, hint: impl Into<EcoString>) {
215        self.hints.push(hint.into());
216    }
217
218    /// Adds a single hint to the diagnostic.
219    pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
220        self.hint(hint);
221        self
222    }
223
224    /// Adds user-facing hints to the diagnostic.
225    pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
226        self.hints.extend(hints);
227        self
228    }
229}
230
231impl From<SyntaxError> for SourceDiagnostic {
232    fn from(error: SyntaxError) -> Self {
233        Self {
234            severity: Severity::Error,
235            span: error.span,
236            message: error.message,
237            trace: eco_vec![],
238            hints: error.hints,
239        }
240    }
241}
242
243/// Destination for a deprecation message when accessing a deprecated value.
244pub trait DeprecationSink {
245    /// Emits the given deprecation message into this sink alongside a version
246    /// in which the deprecated item is planned to be removed.
247    fn emit(self, message: &str, until: Option<&str>);
248}
249
250impl DeprecationSink for () {
251    fn emit(self, _: &str, _: Option<&str>) {}
252}
253
254impl DeprecationSink for (&mut Engine<'_>, Span) {
255    /// Emits the deprecation message as a warning.
256    fn emit(self, message: &str, version: Option<&str>) {
257        self.0
258            .sink
259            .warn(SourceDiagnostic::warning(self.1, message).with_hints(
260                version.map(|v| eco_format!("it will be removed in Typst {}", v)),
261            ));
262    }
263}
264
265/// A part of a diagnostic's [trace](SourceDiagnostic::trace).
266#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
267pub enum Tracepoint {
268    /// A function call.
269    Call(Option<EcoString>),
270    /// A show rule application.
271    Show(EcoString),
272    /// A module import.
273    Import,
274}
275
276impl Display for Tracepoint {
277    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
278        match self {
279            Tracepoint::Call(Some(name)) => {
280                write!(f, "error occurred in this call of function `{name}`")
281            }
282            Tracepoint::Call(None) => {
283                write!(f, "error occurred in this function call")
284            }
285            Tracepoint::Show(name) => {
286                write!(f, "error occurred while applying show rule to this {name}")
287            }
288            Tracepoint::Import => {
289                write!(f, "error occurred while importing this module")
290            }
291        }
292    }
293}
294
295/// Enrich a [`SourceResult`] with a tracepoint.
296pub trait Trace<T> {
297    /// Add the tracepoint to all errors that lie outside the `span`.
298    fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
299    where
300        F: Fn() -> Tracepoint;
301}
302
303impl<T> Trace<T> for SourceResult<T> {
304    fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
305    where
306        F: Fn() -> Tracepoint,
307    {
308        self.map_err(|mut errors| {
309            let Some(trace_range) = world.range(span) else { return errors };
310            for error in errors.make_mut().iter_mut() {
311                // Skip traces that surround the error.
312                if let Some(error_range) = world.range(error.span)
313                    && error.span.id() == span.id()
314                    && trace_range.start <= error_range.start
315                    && trace_range.end >= error_range.end
316                {
317                    continue;
318                }
319
320                error.trace.push(Spanned::new(make_point(), span));
321            }
322            errors
323        })
324    }
325}
326
327/// A result type with a string error message.
328pub type StrResult<T> = Result<T, EcoString>;
329
330/// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by
331/// adding span information.
332pub trait At<T> {
333    /// Add the span information.
334    fn at(self, span: Span) -> SourceResult<T>;
335}
336
337impl<T, S> At<T> for Result<T, S>
338where
339    S: Into<EcoString>,
340{
341    fn at(self, span: Span) -> SourceResult<T> {
342        self.map_err(|message| {
343            let mut diagnostic = SourceDiagnostic::error(span, message);
344            if diagnostic.message.contains("(access denied)") {
345                diagnostic.hint("cannot read file outside of project root");
346                diagnostic
347                    .hint("you can adjust the project root with the --root argument");
348            }
349            eco_vec![diagnostic]
350        })
351    }
352}
353
354/// A result type with a string error message and hints.
355pub type HintedStrResult<T> = Result<T, HintedString>;
356
357/// A string message with hints.
358///
359/// This is internally represented by a vector of strings.
360/// The first element of the vector contains the message.
361/// The remaining elements are the hints.
362/// This is done to reduce the size of a HintedString.
363/// The vector is guaranteed to not be empty.
364#[derive(Debug, Clone, Eq, PartialEq, Hash)]
365pub struct HintedString(EcoVec<EcoString>);
366
367impl HintedString {
368    /// Creates a new hinted string with the given message.
369    pub fn new(message: EcoString) -> Self {
370        Self(eco_vec![message])
371    }
372
373    /// A diagnostic message describing the problem.
374    pub fn message(&self) -> &EcoString {
375        self.0.first().unwrap()
376    }
377
378    /// Additional hints to the user, indicating how this error could be avoided
379    /// or worked around.
380    pub fn hints(&self) -> &[EcoString] {
381        self.0.get(1..).unwrap_or(&[])
382    }
383
384    /// Adds a single hint to the hinted string.
385    pub fn hint(&mut self, hint: impl Into<EcoString>) {
386        self.0.push(hint.into());
387    }
388
389    /// Adds a single hint to the hinted string.
390    pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
391        self.hint(hint);
392        self
393    }
394
395    /// Adds user-facing hints to the hinted string.
396    pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
397        self.0.extend(hints);
398        self
399    }
400}
401
402impl<S> From<S> for HintedString
403where
404    S: Into<EcoString>,
405{
406    fn from(value: S) -> Self {
407        Self::new(value.into())
408    }
409}
410
411impl<T> At<T> for HintedStrResult<T> {
412    fn at(self, span: Span) -> SourceResult<T> {
413        self.map_err(|err| {
414            let mut components = err.0.into_iter();
415            let message = components.next().unwrap();
416            let diag = SourceDiagnostic::error(span, message).with_hints(components);
417            eco_vec![diag]
418        })
419    }
420}
421
422/// Enrich a [`StrResult`] or [`HintedStrResult`] with a hint.
423pub trait Hint<T> {
424    /// Add the hint.
425    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
426}
427
428impl<T, S> Hint<T> for Result<T, S>
429where
430    S: Into<EcoString>,
431{
432    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
433        self.map_err(|message| HintedString::new(message.into()).with_hint(hint))
434    }
435}
436
437impl<T> Hint<T> for HintedStrResult<T> {
438    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
439        self.map_err(|mut error| {
440            error.hint(hint.into());
441            error
442        })
443    }
444}
445
446/// A result type with a file-related error.
447pub type FileResult<T> = Result<T, FileError>;
448
449/// An error that occurred while trying to load of a file.
450#[derive(Debug, Clone, Eq, PartialEq, Hash)]
451pub enum FileError {
452    /// A file was not found at this path.
453    NotFound(PathBuf),
454    /// A file could not be accessed.
455    AccessDenied,
456    /// A directory was found, but a file was expected.
457    IsDirectory,
458    /// The file is not a Typst source file, but should have been.
459    NotSource,
460    /// The file was not valid UTF-8, but should have been.
461    InvalidUtf8,
462    /// The package the file is part of could not be loaded.
463    Package(PackageError),
464    /// Another error.
465    ///
466    /// The optional string can give more details, if available.
467    Other(Option<EcoString>),
468}
469
470impl FileError {
471    /// Create a file error from an I/O error.
472    pub fn from_io(err: io::Error, path: &Path) -> Self {
473        match err.kind() {
474            io::ErrorKind::NotFound => Self::NotFound(path.into()),
475            io::ErrorKind::PermissionDenied => Self::AccessDenied,
476            io::ErrorKind::InvalidData
477                if err.to_string().contains("stream did not contain valid UTF-8") =>
478            {
479                Self::InvalidUtf8
480            }
481            _ => Self::Other(Some(eco_format!("{err}"))),
482        }
483    }
484}
485
486impl std::error::Error for FileError {}
487
488impl Display for FileError {
489    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
490        match self {
491            Self::NotFound(path) => {
492                write!(f, "file not found (searched at {})", path.display())
493            }
494            Self::AccessDenied => f.pad("failed to load file (access denied)"),
495            Self::IsDirectory => f.pad("failed to load file (is a directory)"),
496            Self::NotSource => f.pad("not a Typst source file"),
497            Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
498            Self::Package(error) => error.fmt(f),
499            Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
500            Self::Other(None) => f.pad("failed to load file"),
501        }
502    }
503}
504
505impl From<Utf8Error> for FileError {
506    fn from(_: Utf8Error) -> Self {
507        Self::InvalidUtf8
508    }
509}
510
511impl From<FromUtf8Error> for FileError {
512    fn from(_: FromUtf8Error) -> Self {
513        Self::InvalidUtf8
514    }
515}
516
517impl From<PackageError> for FileError {
518    fn from(err: PackageError) -> Self {
519        Self::Package(err)
520    }
521}
522
523impl From<FileError> for EcoString {
524    fn from(err: FileError) -> Self {
525        eco_format!("{err}")
526    }
527}
528
529/// A result type with a package-related error.
530pub type PackageResult<T> = Result<T, PackageError>;
531
532/// An error that occurred while trying to load a package.
533///
534/// Some variants have an optional string can give more details, if available.
535#[derive(Debug, Clone, Eq, PartialEq, Hash)]
536pub enum PackageError {
537    /// The specified package does not exist.
538    NotFound(PackageSpec),
539    /// The specified package found, but the version does not exist.
540    VersionNotFound(PackageSpec, PackageVersion),
541    /// Failed to retrieve the package through the network.
542    NetworkFailed(Option<EcoString>),
543    /// The package archive was malformed.
544    MalformedArchive(Option<EcoString>),
545    /// Another error.
546    Other(Option<EcoString>),
547}
548
549impl std::error::Error for PackageError {}
550
551impl Display for PackageError {
552    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
553        match self {
554            Self::NotFound(spec) => {
555                write!(f, "package not found (searched for {spec})",)
556            }
557            Self::VersionNotFound(spec, latest) => {
558                write!(
559                    f,
560                    "package found, but version {} does not exist (latest is {})",
561                    spec.version, latest,
562                )
563            }
564            Self::NetworkFailed(Some(err)) => {
565                write!(f, "failed to download package ({err})")
566            }
567            Self::NetworkFailed(None) => f.pad("failed to download package"),
568            Self::MalformedArchive(Some(err)) => {
569                write!(f, "failed to decompress package ({err})")
570            }
571            Self::MalformedArchive(None) => {
572                f.pad("failed to decompress package (archive malformed)")
573            }
574            Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
575            Self::Other(None) => f.pad("failed to load package"),
576        }
577    }
578}
579
580impl From<PackageError> for EcoString {
581    fn from(err: PackageError) -> Self {
582        eco_format!("{err}")
583    }
584}
585
586/// A result type with a data-loading-related error.
587pub type LoadResult<T> = Result<T, LoadError>;
588
589/// A call site independent error that occurred during data loading. This avoids
590/// polluting the memoization with [`Span`]s and [`FileId`]s from source files.
591/// Can be turned into a [`SourceDiagnostic`] using the [`LoadedWithin::within`]
592/// method available on [`LoadResult`].
593///
594/// [`FileId`]: typst_syntax::FileId
595#[derive(Debug, Clone, Eq, PartialEq, Hash)]
596pub struct LoadError {
597    /// The position in the file at which the error occurred.
598    pos: ReportPos,
599    /// Must contain a message formatted like this: `"failed to do thing (cause)"`.
600    message: EcoString,
601}
602
603impl LoadError {
604    /// Creates a new error from a position in a file, a base message
605    /// (e.g. `failed to parse JSON`) and a concrete error (e.g. `invalid
606    /// number`)
607    pub fn new(
608        pos: impl Into<ReportPos>,
609        message: impl std::fmt::Display,
610        error: impl std::fmt::Display,
611    ) -> Self {
612        Self {
613            pos: pos.into(),
614            message: eco_format!("{message} ({error})"),
615        }
616    }
617}
618
619impl From<Utf8Error> for LoadError {
620    fn from(err: Utf8Error) -> Self {
621        let start = err.valid_up_to();
622        let end = start + err.error_len().unwrap_or(0);
623        LoadError::new(
624            start..end,
625            "failed to convert to string",
626            "file is not valid utf-8",
627        )
628    }
629}
630
631/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`]
632/// context.
633pub trait LoadedWithin<T> {
634    /// Report an error, possibly in an external file.
635    fn within(self, loaded: &Loaded) -> SourceResult<T>;
636}
637
638impl<T, E> LoadedWithin<T> for Result<T, E>
639where
640    E: Into<LoadError>,
641{
642    fn within(self, loaded: &Loaded) -> SourceResult<T> {
643        self.map_err(|err| {
644            let LoadError { pos, message } = err.into();
645            load_err_in_text(loaded, pos, message)
646        })
647    }
648}
649
650/// Report an error, possibly in an external file. This will delegate to
651/// [`load_err_in_invalid_text`] if the data isn't valid utf-8.
652fn load_err_in_text(
653    loaded: &Loaded,
654    pos: impl Into<ReportPos>,
655    mut message: EcoString,
656) -> EcoVec<SourceDiagnostic> {
657    let pos = pos.into();
658    // This also does utf-8 validation. Only report an error in an external
659    // file if it is human readable (valid utf-8), otherwise fall back to
660    // `load_err_in_invalid_text`.
661    let lines = Lines::try_from(&loaded.data);
662    match (loaded.source.v, lines) {
663        (LoadSource::Path(file_id), Ok(lines)) => {
664            if let Some(range) = pos.range(&lines) {
665                let span = Span::from_range(file_id, range);
666                return eco_vec![SourceDiagnostic::error(span, message)];
667            }
668
669            // Either `ReportPos::None` was provided, or resolving the range
670            // from the line/column failed. If present report the possibly
671            // wrong line/column in the error message anyway.
672            let span = Span::from_range(file_id, 0..loaded.data.len());
673            if let Some(pair) = pos.line_col(&lines) {
674                message.pop();
675                let (line, col) = pair.numbers();
676                write!(&mut message, " at {line}:{col})").ok();
677            }
678            eco_vec![SourceDiagnostic::error(span, message)]
679        }
680        (LoadSource::Bytes, Ok(lines)) => {
681            if let Some(pair) = pos.line_col(&lines) {
682                message.pop();
683                let (line, col) = pair.numbers();
684                write!(&mut message, " at {line}:{col})").ok();
685            }
686            eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
687        }
688        _ => load_err_in_invalid_text(loaded, pos, message),
689    }
690}
691
692/// Report an error (possibly from an external file) that isn't valid utf-8.
693fn load_err_in_invalid_text(
694    loaded: &Loaded,
695    pos: impl Into<ReportPos>,
696    mut message: EcoString,
697) -> EcoVec<SourceDiagnostic> {
698    let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers());
699    match (loaded.source.v, line_col) {
700        (LoadSource::Path(file), _) => {
701            message.pop();
702            if let Some(package) = file.package() {
703                write!(
704                    &mut message,
705                    " in {package}{}",
706                    file.vpath().as_rooted_path().display()
707                )
708                .ok();
709            } else {
710                write!(&mut message, " in {}", file.vpath().as_rootless_path().display())
711                    .ok();
712            };
713            if let Some((line, col)) = line_col {
714                write!(&mut message, ":{line}:{col}").ok();
715            }
716            message.push(')');
717        }
718        (LoadSource::Bytes, Some((line, col))) => {
719            message.pop();
720            write!(&mut message, " at {line}:{col})").ok();
721        }
722        (LoadSource::Bytes, None) => (),
723    }
724    eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
725}
726
727/// A position at which an error was reported.
728#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
729pub enum ReportPos {
730    /// Contains a range, and a line/column pair.
731    Full(std::ops::Range<u32>, LineCol),
732    /// Contains a range.
733    Range(std::ops::Range<u32>),
734    /// Contains a line/column pair.
735    LineCol(LineCol),
736    #[default]
737    None,
738}
739
740impl From<std::ops::Range<usize>> for ReportPos {
741    fn from(value: std::ops::Range<usize>) -> Self {
742        Self::Range(value.start.saturating_as()..value.end.saturating_as())
743    }
744}
745
746impl From<LineCol> for ReportPos {
747    fn from(value: LineCol) -> Self {
748        Self::LineCol(value)
749    }
750}
751
752impl ReportPos {
753    /// Creates a position from a pre-existing range and line-column pair.
754    pub fn full(range: std::ops::Range<usize>, pair: LineCol) -> Self {
755        let range = range.start.saturating_as()..range.end.saturating_as();
756        Self::Full(range, pair)
757    }
758
759    /// Tries to determine the byte range for this position.
760    fn range(&self, lines: &Lines<String>) -> Option<std::ops::Range<usize>> {
761        match self {
762            ReportPos::Full(range, _) => Some(range.start as usize..range.end as usize),
763            ReportPos::Range(range) => Some(range.start as usize..range.end as usize),
764            &ReportPos::LineCol(pair) => {
765                let i =
766                    lines.line_column_to_byte(pair.line as usize, pair.col as usize)?;
767                Some(i..i)
768            }
769            ReportPos::None => None,
770        }
771    }
772
773    /// Tries to determine the line/column for this position.
774    fn line_col(&self, lines: &Lines<String>) -> Option<LineCol> {
775        match self {
776            &ReportPos::Full(_, pair) => Some(pair),
777            ReportPos::Range(range) => {
778                let (line, col) = lines.byte_to_line_column(range.start as usize)?;
779                Some(LineCol::zero_based(line, col))
780            }
781            &ReportPos::LineCol(pair) => Some(pair),
782            ReportPos::None => None,
783        }
784    }
785
786    /// Either gets the line/column pair, or tries to compute it from possibly
787    /// invalid utf-8 data.
788    fn try_line_col(&self, bytes: &[u8]) -> Option<LineCol> {
789        match self {
790            &ReportPos::Full(_, pair) => Some(pair),
791            ReportPos::Range(range) => {
792                LineCol::try_from_byte_pos(range.start as usize, bytes)
793            }
794            &ReportPos::LineCol(pair) => Some(pair),
795            ReportPos::None => None,
796        }
797    }
798}
799
800/// A line/column pair.
801#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
802pub struct LineCol {
803    /// The 0-based line.
804    line: u32,
805    /// The 0-based column.
806    col: u32,
807}
808
809impl LineCol {
810    /// Constructs the line/column pair from 0-based indices.
811    pub fn zero_based(line: usize, col: usize) -> Self {
812        Self {
813            line: line.saturating_as(),
814            col: col.saturating_as(),
815        }
816    }
817
818    /// Constructs the line/column pair from 1-based numbers.
819    pub fn one_based(line: usize, col: usize) -> Self {
820        Self::zero_based(line.saturating_sub(1), col.saturating_sub(1))
821    }
822
823    /// Try to compute a line/column pair from possibly invalid utf-8 data.
824    pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> {
825        let bytes = &bytes[..pos];
826        let mut line = 0;
827        #[allow(clippy::double_ended_iterator_last)]
828        let line_start = memchr::memchr_iter(b'\n', bytes)
829            .inspect(|_| line += 1)
830            .last()
831            .map(|i| i + 1)
832            .unwrap_or(bytes.len());
833
834        let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count();
835        Some(LineCol::zero_based(line, col))
836    }
837
838    /// Returns the 0-based line/column indices.
839    pub fn indices(&self) -> (usize, usize) {
840        (self.line as usize, self.col as usize)
841    }
842
843    /// Returns the 1-based line/column numbers.
844    pub fn numbers(&self) -> (usize, usize) {
845        (self.line as usize + 1, self.col as usize + 1)
846    }
847}
848
849/// Format a user-facing error message for an XML-like file format.
850pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError {
851    let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
852    let message = match error {
853        roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
854            eco_format!(
855                "failed to parse {format} (found closing tag '{actual}' instead of '{expected}')"
856            )
857        }
858        roxmltree::Error::UnknownEntityReference(entity, _) => {
859            eco_format!("failed to parse {format} (unknown entity '{entity}')")
860        }
861        roxmltree::Error::DuplicatedAttribute(attr, _) => {
862            eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
863        }
864        roxmltree::Error::NoRootNode => {
865            eco_format!("failed to parse {format} (missing root node)")
866        }
867        err => eco_format!("failed to parse {format} ({err})"),
868    };
869
870    LoadError { pos: pos.into(), message }
871}
872
873/// Asserts a condition, generating an internal compiler error with the provided
874/// message on failure.
875#[track_caller]
876pub fn assert_internal(cond: bool, msg: &str) -> HintedStrResult<()> {
877    if !cond { Err(internal_error(msg)) } else { Ok(()) }
878}
879
880/// Generates an internal compiler error with the provided message.
881#[track_caller]
882pub fn panic_internal(msg: &str) -> HintedStrResult<()> {
883    Err(internal_error(msg))
884}
885
886/// Adds a method analogous to [`Option::expect`] that raises an internal
887/// compiler error instead of panicking.
888pub trait ExpectInternal<T> {
889    /// Extracts the value, producing an internal error if `self` is `None`.
890    fn expect_internal(self, msg: &str) -> HintedStrResult<T>;
891}
892
893impl<T> ExpectInternal<T> for Option<T> {
894    #[track_caller]
895    fn expect_internal(self, msg: &str) -> HintedStrResult<T> {
896        match self {
897            Some(val) => Ok(val),
898            None => Err(internal_error(msg)),
899        }
900    }
901}
902
903/// The shared internal implementation of [`assert_internal`] and
904/// [`expect_internal`].
905#[track_caller]
906fn internal_error(msg: &str) -> HintedString {
907    let loc = std::panic::Location::caller();
908    let mut error = error!(
909        "internal error: {msg} (occurred at {loc})";
910        hint: "please report this as a bug"
911    );
912
913    if cfg!(debug_assertions) {
914        let backtrace = Backtrace::capture();
915        if backtrace.status() == BacktraceStatus::Captured {
916            error.hint(eco_format!("compiler backtrace:\n{backtrace}"));
917        } else {
918            error.hint("set `RUST_BACKTRACE` to `1` or `full` to capture a backtrace");
919        }
920    }
921
922    error
923}