1use 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#[macro_export]
43#[doc(hidden)]
44macro_rules! __bail {
45 (
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 ($error:expr) => {
59 return Err(::ecow::eco_vec![$error])
60 };
61
62 ($($tts:tt)*) => {
64 return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
65 };
66}
67
68#[macro_export]
71#[doc(hidden)]
72macro_rules! __error {
73 ($fmt:literal $(, $arg:expr)* $(,)?) => {
75 $crate::diag::eco_format!($fmt, $($arg),*).into()
76 };
77
78 (
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 (
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#[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
143pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
145
146#[derive(Debug, Clone, Eq, PartialEq, Hash)]
148pub struct Warned<T> {
149 pub output: T,
151 pub warnings: EcoVec<SourceDiagnostic>,
153}
154
155impl<T> Warned<T> {
156 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#[derive(Debug, Clone, Eq, PartialEq, Hash)]
167pub struct SourceDiagnostic {
168 pub severity: Severity,
170 pub span: Span,
172 pub message: EcoString,
174 pub trace: EcoVec<Spanned<Tracepoint>>,
176 pub hints: EcoVec<EcoString>,
179}
180
181#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
183pub enum Severity {
184 Error,
186 Warning,
188}
189
190impl SourceDiagnostic {
191 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 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 pub fn hint(&mut self, hint: impl Into<EcoString>) {
215 self.hints.push(hint.into());
216 }
217
218 pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
220 self.hint(hint);
221 self
222 }
223
224 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
243pub trait DeprecationSink {
245 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 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#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
267pub enum Tracepoint {
268 Call(Option<EcoString>),
270 Show(EcoString),
272 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
295pub trait Trace<T> {
297 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 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
327pub type StrResult<T> = Result<T, EcoString>;
329
330pub trait At<T> {
333 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
354pub type HintedStrResult<T> = Result<T, HintedString>;
356
357#[derive(Debug, Clone, Eq, PartialEq, Hash)]
365pub struct HintedString(EcoVec<EcoString>);
366
367impl HintedString {
368 pub fn new(message: EcoString) -> Self {
370 Self(eco_vec![message])
371 }
372
373 pub fn message(&self) -> &EcoString {
375 self.0.first().unwrap()
376 }
377
378 pub fn hints(&self) -> &[EcoString] {
381 self.0.get(1..).unwrap_or(&[])
382 }
383
384 pub fn hint(&mut self, hint: impl Into<EcoString>) {
386 self.0.push(hint.into());
387 }
388
389 pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
391 self.hint(hint);
392 self
393 }
394
395 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
422pub trait Hint<T> {
424 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
446pub type FileResult<T> = Result<T, FileError>;
448
449#[derive(Debug, Clone, Eq, PartialEq, Hash)]
451pub enum FileError {
452 NotFound(PathBuf),
454 AccessDenied,
456 IsDirectory,
458 NotSource,
460 InvalidUtf8,
462 Package(PackageError),
464 Other(Option<EcoString>),
468}
469
470impl FileError {
471 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
529pub type PackageResult<T> = Result<T, PackageError>;
531
532#[derive(Debug, Clone, Eq, PartialEq, Hash)]
536pub enum PackageError {
537 NotFound(PackageSpec),
539 VersionNotFound(PackageSpec, PackageVersion),
541 NetworkFailed(Option<EcoString>),
543 MalformedArchive(Option<EcoString>),
545 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
586pub type LoadResult<T> = Result<T, LoadError>;
588
589#[derive(Debug, Clone, Eq, PartialEq, Hash)]
596pub struct LoadError {
597 pos: ReportPos,
599 message: EcoString,
601}
602
603impl LoadError {
604 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
631pub trait LoadedWithin<T> {
634 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
650fn 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 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 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
692fn 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#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
729pub enum ReportPos {
730 Full(std::ops::Range<u32>, LineCol),
732 Range(std::ops::Range<u32>),
734 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 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 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 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 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#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
802pub struct LineCol {
803 line: u32,
805 col: u32,
807}
808
809impl LineCol {
810 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 pub fn one_based(line: usize, col: usize) -> Self {
820 Self::zero_based(line.saturating_sub(1), col.saturating_sub(1))
821 }
822
823 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 pub fn indices(&self) -> (usize, usize) {
840 (self.line as usize, self.col as usize)
841 }
842
843 pub fn numbers(&self) -> (usize, usize) {
845 (self.line as usize + 1, self.col as usize + 1)
846 }
847}
848
849pub 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#[track_caller]
876pub fn assert_internal(cond: bool, msg: &str) -> HintedStrResult<()> {
877 if !cond { Err(internal_error(msg)) } else { Ok(()) }
878}
879
880#[track_caller]
882pub fn panic_internal(msg: &str) -> HintedStrResult<()> {
883 Err(internal_error(msg))
884}
885
886pub trait ExpectInternal<T> {
889 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#[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}