1use std::collections::{BTreeMap, BTreeSet};
4use std::num::NonZeroUsize;
5use std::ops::Range;
6
7use ecow::EcoVec;
8#[cfg(feature = "parallel")]
9use rayon::prelude::*;
10use typst::diag::{Severity, SourceDiagnostic, Tracepoint, Warned};
11use typst::foundations::{Bytes, Dict, Repr, Smart};
12use typst::syntax::{DiagSpan, Span};
13use typst::{Feature, World, WorldExt};
14use typst_layout::PagedDocument;
15use typst_pdf::{PdfOptions, PdfStandard, PdfStandards, Timestamp};
16
17use super::fulfillment::{
18 CompilationFulfillmentReport, CompilationFulfillmentSet, FontFulfillmentReport,
19 InvalidCompilationFulfillmentSet, PackageFulfillmentReport, verify_compilation_fulfillment_set,
20};
21use super::identity::{DiagnosticProducer, ImplementationIdentity};
22use crate::domain::{DocumentTime, TypstTarget};
23use crate::embedded::EmbeddedTypst;
24use crate::limits::{LimitError, Limits, ResourceKind};
25use crate::payload::SharedBytes;
26use crate::world::PackWorld;
27use crate::world_trace::{WorldTrace, logical_path};
28use crate::{CanonicalIdentity, CanonicalIdentityRole, Pack};
29
30pub type CompilationResource = ResourceKind<3>;
32
33#[allow(non_upper_case_globals)]
34impl ResourceKind<3> {
35 pub const SourcePages: Self = Self::new(0);
36 pub const Artifacts: Self = Self::new(1);
37 pub const PixelsPerArtifact: Self = Self::new(2);
38 pub const TotalPixels: Self = Self::new(3);
39 pub const ArtifactBytes: Self = Self::new(4);
40 pub const RetainedArtifactBytes: Self = Self::new(5);
41 pub const ExportWorkers: Self = Self::new(6);
42}
43
44pub type CompilationLimitError = LimitError<CompilationResource>;
46
47pub type CompilationLimits = Limits<CompilationResource>;
49
50impl Limits<CompilationResource> {
51 #[track_caller]
53 pub fn new(
54 source_pages: u64,
55 artifacts: u64,
56 pixels_per_artifact: u64,
57 total_pixels: u64,
58 artifact_bytes: u64,
59 retained_artifact_bytes: u64,
60 export_workers: u64,
61 ) -> Self {
62 let limits = Self::from_ceilings([
63 source_pages,
64 artifacts,
65 pixels_per_artifact,
66 total_pixels,
67 artifact_bytes,
68 retained_artifact_bytes,
69 export_workers,
70 ])
71 .assert_probe_resources([
72 CompilationResource::SourcePages,
73 CompilationResource::Artifacts,
74 CompilationResource::PixelsPerArtifact,
75 CompilationResource::TotalPixels,
76 CompilationResource::ArtifactBytes,
77 CompilationResource::RetainedArtifactBytes,
78 CompilationResource::ExportWorkers,
79 ]);
80 assert!(
81 export_workers > 0,
82 "the ExportWorkers ceiling must be greater than zero"
83 );
84 limits
85 }
86
87 pub const fn reference_v1() -> Self {
89 Self::from_ceilings([
90 10_000,
91 10_000,
92 100_000_000,
93 1_000_000_000,
94 512 * 1024 * 1024,
95 2 * 1024 * 1024 * 1024,
96 4,
97 ])
98 }
99
100 #[track_caller]
102 pub fn with_export_workers(self, export_workers: u64) -> Self {
103 Self::new(
104 self.source_pages(),
105 self.artifacts(),
106 self.pixels_per_artifact(),
107 self.total_pixels(),
108 self.artifact_bytes(),
109 self.retained_artifact_bytes(),
110 export_workers,
111 )
112 }
113
114 pub const fn source_pages(&self) -> u64 {
115 self.ceilings[0]
116 }
117
118 pub const fn artifacts(&self) -> u64 {
119 self.ceilings[1]
120 }
121
122 pub const fn pixels_per_artifact(&self) -> u64 {
123 self.ceilings[2]
124 }
125
126 pub const fn total_pixels(&self) -> u64 {
127 self.ceilings[3]
128 }
129
130 pub const fn artifact_bytes(&self) -> u64 {
131 self.ceilings[4]
132 }
133
134 pub const fn retained_artifact_bytes(&self) -> u64 {
135 self.ceilings[5]
136 }
137
138 pub const fn export_workers(&self) -> u64 {
139 self.ceilings[6]
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145pub enum OutputFormat {
146 Pdf,
147 Png,
148 Svg,
149 Html,
152}
153
154impl OutputFormat {
155 pub fn extension(self) -> &'static str {
157 match self {
158 Self::Pdf => "pdf",
159 Self::Png => "png",
160 Self::Svg => "svg",
161 Self::Html => "html",
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
168pub struct PdfOutputSpecification {
169 pub page_selection: PageSelection,
171 pub standards: Vec<PdfStandard>,
173 pub identifier: Smart<String>,
175 pub creator: Smart<Option<String>>,
177 pub tags: Smart<bool>,
182 pub creation_timestamp: CreationTimestamp,
184 pub pretty: bool,
186}
187
188impl Default for PdfOutputSpecification {
189 fn default() -> Self {
190 let pdf = PdfOptions::default();
191 Self {
192 page_selection: PageSelection::default(),
193 standards: vec![],
194 identifier: pdf.ident,
195 creator: pdf.creator,
196 tags: Smart::Auto,
197 creation_timestamp: CreationTimestamp::Automatic,
198 pretty: pdf.pretty,
199 }
200 }
201}
202
203#[derive(Debug, Clone, Default)]
205pub struct PngOutputSpecification {
206 pub page_selection: PageSelection,
208 pub pixels_per_inch: Option<f64>,
210 pub render_bleed: bool,
212}
213
214#[derive(Debug, Clone, Default)]
216pub struct SvgOutputSpecification {
217 pub page_selection: PageSelection,
219 pub render_bleed: bool,
221 pub pretty: bool,
223}
224
225#[derive(Debug, Clone, Default)]
227pub struct HtmlOutputSpecification {
228 pub pretty: bool,
230}
231
232#[derive(Debug, Clone)]
234pub enum CompilationOutputSpecification {
235 Pdf(PdfOutputSpecification),
236 Png(PngOutputSpecification),
237 Svg(SvgOutputSpecification),
238 Html(HtmlOutputSpecification),
239}
240
241impl CompilationOutputSpecification {
242 pub fn format(&self) -> OutputFormat {
244 match self {
245 Self::Pdf(_) => OutputFormat::Pdf,
246 Self::Png(_) => OutputFormat::Png,
247 Self::Svg(_) => OutputFormat::Svg,
248 Self::Html(_) => OutputFormat::Html,
249 }
250 }
251
252 fn target(&self) -> TypstTarget {
253 match self {
254 Self::Html(_) => TypstTarget::Html,
255 Self::Pdf(_) | Self::Png(_) | Self::Svg(_) => TypstTarget::Paged,
256 }
257 }
258}
259
260#[derive(Debug, Clone)]
288pub struct PackOverrideSet {
289 pack_identity: CanonicalIdentity,
290 project_paths: BTreeSet<String>,
291 replacements: BTreeMap<String, Bytes>,
292}
293
294impl PackOverrideSet {
295 pub fn new(pack: &Pack) -> Self {
297 Self {
298 pack_identity: pack.identity(),
299 project_paths: pack.files().map(|(path, _)| path.to_owned()).collect(),
300 replacements: BTreeMap::new(),
301 }
302 }
303
304 pub fn validate_paths<I, S>(pack: &Pack, paths: I) -> Result<(), PackOverrideSetError>
306 where
307 I: IntoIterator<Item = S>,
308 S: AsRef<str>,
309 {
310 let project_paths = pack
311 .files()
312 .map(|(path, _)| path.to_owned())
313 .collect::<BTreeSet<_>>();
314 let mut validated = BTreeSet::new();
315 for supplied in paths {
316 let path = validate_override_path(
317 &project_paths,
318 |path| validated.contains(path),
319 supplied.as_ref(),
320 )?;
321 validated.insert(path);
322 }
323 Ok(())
324 }
325
326 pub fn replace(
328 mut self,
329 path: impl AsRef<str>,
330 data: impl Into<Vec<u8>>,
331 ) -> Result<Self, PackOverrideSetError> {
332 let path = validate_override_path(
333 &self.project_paths,
334 |path| self.replacements.contains_key(path),
335 path.as_ref(),
336 )?;
337 self.replacements.insert(path, Bytes::new(data.into()));
338 Ok(self)
339 }
340}
341
342fn validate_override_path(
343 project_paths: &BTreeSet<String>,
344 is_declared: impl FnOnce(&str) -> bool,
345 supplied: &str,
346) -> Result<String, PackOverrideSetError> {
347 let path = Pack::canonical_project_path(supplied).map_err(|message| {
348 PackOverrideSetError::InvalidProjectPath {
349 path: supplied.to_owned(),
350 message,
351 }
352 })?;
353 if is_declared(&path) {
354 return Err(PackOverrideSetError::DuplicateProjectPath { path });
355 }
356 if !project_paths.contains(&path) {
357 return Err(PackOverrideSetError::MissingProjectPath { path });
358 }
359 Ok(path)
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
364pub enum PackOverrideSetError {
365 #[error("invalid Pack Override project path `{path}`: {message}")]
366 InvalidProjectPath { path: String, message: String },
367 #[error("Pack Override path `{path}` is declared more than once")]
368 DuplicateProjectPath { path: String },
369 #[error("Pack Override path `{path}` is not a contained project file")]
370 MissingProjectPath { path: String },
371}
372
373pub struct PackCompilationRequest {
378 pack: Pack,
379 output_specification: CompilationOutputSpecification,
380 inputs: Dict,
381 overrides: PackOverrideSet,
382 features: Vec<Feature>,
383 document_time: DocumentTime,
384 fulfillments: CompilationFulfillmentSet,
385}
386
387impl PackCompilationRequest {
388 pub fn new(pack: Pack, output_specification: CompilationOutputSpecification) -> Self {
390 let overrides = PackOverrideSet::new(&pack);
391 Self {
392 pack,
393 output_specification,
394 inputs: Dict::new(),
395 overrides,
396 features: Vec::new(),
397 document_time: DocumentTime::Absent,
398 fulfillments: CompilationFulfillmentSet::empty(),
399 }
400 }
401
402 pub fn output(mut self, output_specification: CompilationOutputSpecification) -> Self {
404 self.output_specification = output_specification;
405 self
406 }
407
408 pub fn inputs(mut self, inputs: Dict) -> Self {
410 self.inputs = inputs;
411 self
412 }
413
414 pub fn overrides(mut self, overrides: PackOverrideSet) -> Self {
416 self.overrides = overrides;
417 self
418 }
419
420 pub fn feature(mut self, feature: Feature) -> Self {
422 self.features.push(feature);
423 self
424 }
425
426 pub fn document_time(mut self, document_time: DocumentTime) -> Self {
428 self.document_time = document_time;
429 self
430 }
431
432 pub fn fulfillments(mut self, fulfillments: CompilationFulfillmentSet) -> Self {
434 self.fulfillments = fulfillments;
435 self
436 }
437}
438
439#[derive(Debug, Clone, Copy, Default, Hash)]
441pub enum CreationTimestamp {
442 #[default]
444 Automatic,
445 Explicit(Timestamp),
447 Omit,
449}
450
451pub type PageRange = std::ops::RangeInclusive<Option<NonZeroUsize>>;
453
454#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
459pub struct PageSelection {
460 ranges: Vec<PageRange>,
461}
462
463impl PageSelection {
464 pub fn all() -> Self {
466 Self::default()
467 }
468
469 pub fn new(ranges: Vec<PageRange>) -> Self {
473 Self { ranges }
474 }
475
476 pub fn ranges(&self) -> &[PageRange] {
478 &self.ranges
479 }
480
481 fn typst_page_ranges(&self) -> Option<typst::layout::PageRanges> {
482 (!self.ranges.is_empty()).then(|| typst::layout::PageRanges::new(self.ranges.clone()))
483 }
484}
485
486pub fn parse_page_selection(text: &str) -> Result<PageSelection, String> {
488 text.split(',')
489 .map(|part| {
490 let part = part.trim();
491 let parse = |value: &str| -> Result<NonZeroUsize, String> {
492 if value == "0" {
493 Err("page numbers start at one".to_owned())
494 } else {
495 value
496 .parse::<NonZeroUsize>()
497 .map_err(|_| format!("`{value}` is not a valid page number"))
498 }
499 };
500 match part
501 .split('-')
502 .map(str::trim)
503 .collect::<Vec<_>>()
504 .as_slice()
505 {
506 [] | [""] => Err("page export range must not be empty".to_owned()),
507 [single] => {
508 let page = parse(single)?;
509 Ok(Some(page)..=Some(page))
510 }
511 ["", ""] => Err("page export range must have start or end".to_owned()),
512 [start, ""] => Ok(Some(parse(start)?)..=None),
513 ["", end] => Ok(None..=Some(parse(end)?)),
514 [start, end] => {
515 let start = parse(start)?;
516 let end = parse(end)?;
517 if start > end {
518 Err("page export range must end at a page after the start".to_owned())
519 } else {
520 Ok(Some(start)..=Some(end))
521 }
522 }
523 _ => Err("page export range must have a single hyphen".to_owned()),
524 }
525 })
526 .collect::<Result<Vec<_>, _>>()
527 .map(PageSelection::new)
528}
529
530#[derive(Debug, Clone)]
532pub struct CompilationArtifact {
533 format: OutputFormat,
534 bytes: SharedBytes,
535 source_page_number: Option<NonZeroUsize>,
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
540pub enum CompilationStatus {
541 Succeeded,
542 Rejected,
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
547pub enum DiagnosticPhase {
548 Compilation,
549 Export,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
554pub enum DiagnosticSeverity {
555 Error,
556 Warning,
557}
558
559#[derive(Debug, Clone, PartialEq, Eq, Hash)]
561pub struct LogicalSpan {
562 logical_path: Option<String>,
563 byte_range: Option<Range<usize>>,
564}
565
566impl LogicalSpan {
567 pub fn logical_path(&self) -> Option<&str> {
569 self.logical_path.as_deref()
570 }
571
572 pub fn byte_range(&self) -> Option<&Range<usize>> {
574 self.byte_range.as_ref()
575 }
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Hash)]
580pub struct DiagnosticHint {
581 message: String,
582 span: LogicalSpan,
583}
584
585impl DiagnosticHint {
586 pub fn message(&self) -> &str {
587 &self.message
588 }
589
590 pub fn span(&self) -> &LogicalSpan {
591 &self.span
592 }
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
597pub enum TracepointKind {
598 Call,
599 Show,
600 Import,
601 Include,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Hash)]
606pub struct DiagnosticTracepoint {
607 kind: TracepointKind,
608 value: Option<String>,
609 span: LogicalSpan,
610}
611
612impl DiagnosticTracepoint {
613 pub fn kind(&self) -> TracepointKind {
614 self.kind
615 }
616
617 pub fn value(&self) -> Option<&str> {
618 self.value.as_deref()
619 }
620
621 pub fn span(&self) -> &LogicalSpan {
622 &self.span
623 }
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Hash)]
628pub struct CompilationDiagnostic {
629 severity: DiagnosticSeverity,
630 message: String,
631 span: LogicalSpan,
632 hints: Vec<DiagnosticHint>,
633 trace: Vec<DiagnosticTracepoint>,
634 phase: DiagnosticPhase,
635 producer: DiagnosticProducer,
636 source_page_number: Option<NonZeroUsize>,
637}
638
639impl CompilationDiagnostic {
640 pub fn severity(&self) -> DiagnosticSeverity {
641 self.severity
642 }
643
644 pub fn message(&self) -> &str {
645 &self.message
646 }
647
648 pub fn span(&self) -> &LogicalSpan {
649 &self.span
650 }
651
652 pub fn hints(&self) -> &[DiagnosticHint] {
653 &self.hints
654 }
655
656 pub fn trace(&self) -> &[DiagnosticTracepoint] {
657 &self.trace
658 }
659
660 pub fn phase(&self) -> DiagnosticPhase {
661 self.phase
662 }
663
664 pub fn producer(&self) -> DiagnosticProducer {
665 self.producer
666 }
667
668 pub fn source_page_number(&self) -> Option<NonZeroUsize> {
670 self.source_page_number
671 }
672}
673
674#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
676pub struct CompilationDocumentSummary {
677 target: TypstTarget,
678 source_page_count: Option<usize>,
679}
680
681impl CompilationDocumentSummary {
682 pub fn target(self) -> TypstTarget {
683 self.target
684 }
685
686 pub fn source_page_count(self) -> Option<usize> {
687 self.source_page_count
688 }
689}
690
691#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
693pub enum CompilationAccessKind {
694 Source,
695 File,
696 Font,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
701pub enum CompilationAccessOutcome {
702 Read {
703 byte_length: usize,
704 digest: [u8; 16],
705 },
706 Missing,
707 Failed,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
712pub struct CompilationAccessObservation {
713 kind: CompilationAccessKind,
714 logical_path: String,
715 font_index: Option<usize>,
716 outcome: CompilationAccessOutcome,
717}
718
719impl CompilationAccessObservation {
720 pub(crate) fn new(
721 kind: CompilationAccessKind,
722 logical_path: String,
723 font_index: Option<usize>,
724 outcome: CompilationAccessOutcome,
725 ) -> Self {
726 Self {
727 kind,
728 logical_path,
729 font_index,
730 outcome,
731 }
732 }
733
734 pub fn kind(&self) -> CompilationAccessKind {
735 self.kind
736 }
737
738 pub fn logical_path(&self) -> &str {
739 &self.logical_path
740 }
741
742 pub fn font_index(&self) -> Option<usize> {
743 self.font_index
744 }
745
746 pub fn outcome(&self) -> &CompilationAccessOutcome {
747 &self.outcome
748 }
749}
750
751#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
753pub struct CompilationAccessTrace {
754 observations: BTreeSet<CompilationAccessObservation>,
755}
756
757impl CompilationAccessTrace {
758 pub fn observations(&self) -> impl Iterator<Item = &CompilationAccessObservation> {
759 self.observations.iter()
760 }
761
762 pub(crate) fn from_observations(observations: BTreeSet<CompilationAccessObservation>) -> Self {
763 Self { observations }
764 }
765}
766
767#[derive(Debug, Clone)]
769pub struct CompilationReport {
770 outcome: CompilationReportOutcome,
771 fulfillments: CompilationFulfillmentReport,
772}
773
774#[derive(Debug, Clone)]
775pub enum CompilationReportOutcome {
776 Result(Box<CompilationResult>),
777 Operation {
778 outcome: CompilationOperationOutcome,
779 compilation_identity: CanonicalIdentity,
780 },
781}
782
783impl CompilationReport {
784 pub fn outcome(&self) -> &CompilationReportOutcome {
785 &self.outcome
786 }
787
788 pub fn result(&self) -> Option<&CompilationResult> {
789 match &self.outcome {
790 CompilationReportOutcome::Result(result) => Some(result.as_ref()),
791 CompilationReportOutcome::Operation { .. } => None,
792 }
793 }
794 pub fn fulfillments(&self) -> &CompilationFulfillmentReport {
795 &self.fulfillments
796 }
797}
798
799#[derive(Debug, Clone)]
801pub struct CompilationResult {
802 status: CompilationStatus,
803 artifacts: Vec<CompilationArtifact>,
804 diagnostics: Vec<CompilationDiagnostic>,
805 pack_warnings: Vec<PackCompilationWarning>,
806 document: CompilationDocumentSummary,
807 access_trace: CompilationAccessTrace,
808 result_identity: CanonicalIdentity,
809 compilation_identity: CanonicalIdentity,
810 engine_identity: ImplementationIdentity,
811 exporter_identity: ImplementationIdentity,
812}
813
814impl CompilationResult {
815 pub fn status(&self) -> CompilationStatus {
816 self.status
817 }
818
819 pub fn artifacts(&self) -> &[CompilationArtifact] {
820 &self.artifacts
821 }
822
823 pub fn diagnostics(&self) -> &[CompilationDiagnostic] {
824 &self.diagnostics
825 }
826
827 pub fn pack_warnings(&self) -> &[PackCompilationWarning] {
829 &self.pack_warnings
830 }
831
832 pub fn source_page_count(&self) -> Option<usize> {
833 self.document.source_page_count
834 }
835
836 pub fn document(&self) -> CompilationDocumentSummary {
837 self.document
838 }
839
840 pub fn access_trace(&self) -> &CompilationAccessTrace {
841 &self.access_trace
842 }
843
844 pub fn result_identity(&self) -> CanonicalIdentity {
845 self.result_identity
846 }
847
848 pub fn compilation_identity(&self) -> CanonicalIdentity {
850 self.compilation_identity
851 }
852
853 pub fn engine_identity(&self) -> ImplementationIdentity {
854 self.engine_identity
855 }
856
857 pub fn exporter_identity(&self) -> ImplementationIdentity {
858 self.exporter_identity
859 }
860}
861
862#[derive(Debug, Clone, PartialEq, Eq, Hash)]
864pub struct PackCompilationWarning {
865 message: String,
866 hints: Vec<String>,
867}
868
869impl PackCompilationWarning {
870 pub fn message(&self) -> &str {
871 &self.message
872 }
873
874 pub fn hints(&self) -> &[String] {
875 &self.hints
876 }
877}
878
879impl CompilationArtifact {
880 pub fn format(&self) -> OutputFormat {
882 self.format
883 }
884
885 pub fn source_page_number(&self) -> Option<NonZeroUsize> {
889 self.source_page_number
890 }
891
892 pub fn bytes(&self) -> &[u8] {
894 self.bytes.as_slice()
895 }
896
897 pub fn into_bytes(self) -> Vec<u8> {
899 self.bytes.into_vec()
900 }
901}
902
903#[derive(Debug, Clone)]
905pub(crate) struct CompilationOutput {
906 pub artifacts: Vec<CompilationArtifact>,
908 pub warnings: EcoVec<SourceDiagnostic>,
910 pack_warnings: EcoVec<SourceDiagnostic>,
911 source_page_count: Option<usize>,
912}
913
914#[derive(Debug, thiserror::Error)]
916pub(crate) enum CompileError {
917 #[error(transparent)]
919 Limit(#[from] CompilationLimitError),
920 #[error(transparent)]
922 InvalidPdfStandards(#[from] PdfStandardsValidationError),
923 #[error("compilation failed with {} error(s)", errors.len())]
926 Diagnostics {
927 errors: EcoVec<SourceDiagnostic>,
928 warnings: EcoVec<SourceDiagnostic>,
929 pack_warnings: EcoVec<SourceDiagnostic>,
930 phase: DiagnosticPhase,
931 source_page_count: Option<usize>,
932 },
933 #[error("PNG export failed for source page {source_page_number}: {message}")]
935 PngExport {
936 message: String,
937 warnings: EcoVec<SourceDiagnostic>,
939 pack_warnings: EcoVec<SourceDiagnostic>,
940 source_page_count: usize,
941 source_page_number: NonZeroUsize,
943 },
944}
945
946#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
948#[error("invalid PDF standards: {message}")]
949pub struct PdfStandardsValidationError {
950 message: String,
951 hints: Vec<String>,
952}
953
954impl PdfStandardsValidationError {
955 pub fn message(&self) -> &str {
957 &self.message
958 }
959
960 pub fn hints(&self) -> &[String] {
962 &self.hints
963 }
964
965 pub fn into_parts(self) -> (String, Vec<String>) {
967 (self.message, self.hints)
968 }
969}
970
971#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
973#[non_exhaustive]
974pub enum CompilationRequestIssue {
975 #[error("page selection range {start}-{end} ends before it starts")]
977 InvalidPageRange {
978 start: NonZeroUsize,
979 end: NonZeroUsize,
980 },
981 #[error("the Typst Bundle feature is not supported for Pack compilation")]
983 UnsupportedBundleFeature,
984 #[error("PNG pixels per inch must be finite and greater than zero")]
986 InvalidPpi,
987 #[error(transparent)]
989 InvalidPdfStandards(PdfStandardsValidationError),
990 #[error("the Pack Override Set is bound to a different Pack")]
992 OverrideSetPackMismatch,
993 #[error("the document-time UNIX timestamp is out of range")]
995 InvalidDocumentTimestamp,
996}
997
998#[derive(Debug)]
1000pub struct CompilationRequestRejection {
1001 issues: Vec<CompilationRequestIssue>,
1002}
1003
1004impl CompilationRequestRejection {
1005 pub fn issues(&self) -> &[CompilationRequestIssue] {
1007 &self.issues
1008 }
1009
1010 fn new(issues: Vec<CompilationRequestIssue>) -> Self {
1011 debug_assert!(!issues.is_empty());
1012 Self { issues }
1013 }
1014}
1015
1016impl std::fmt::Display for CompilationRequestRejection {
1017 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1018 if let [issue] = self.issues.as_slice() {
1019 issue.fmt(formatter)
1020 } else {
1021 formatter.write_str("the compilation request contains multiple invalid values")
1022 }
1023 }
1024}
1025
1026impl std::error::Error for CompilationRequestRejection {}
1027
1028#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1030pub enum CompilationOperationOutcome {
1031 #[error(transparent)]
1032 InvalidFulfillmentSet(InvalidCompilationFulfillmentSet),
1033 #[error(transparent)]
1034 ResourceLimit(CompilationLimitError),
1035}
1036
1037pub(crate) enum PackCompilationPreparation {
1038 Execute {
1039 world: Box<PackWorld>,
1040 kernel: Box<PreparedPackCompilationKernel>,
1041 },
1042 Report(CompilationReport),
1043 Rejected(CompilationRequestRejection),
1044}
1045
1046#[cfg(test)]
1048pub(crate) fn compile_world(
1049 world: &dyn World,
1050 output: &CompilationOutputSpecification,
1051) -> Result<CompilationOutput, CompileError> {
1052 compile_with_default_pdf_timestamp(world, output, CompilationLimits::reference_v1(), || {
1053 world.today(None).map(Timestamp::new_utc)
1054 })
1055}
1056
1057#[allow(clippy::result_large_err)]
1059pub fn compile(
1060 request: PackCompilationRequest,
1061) -> Result<CompilationReport, CompilationRequestRejection> {
1062 compile_with_limits(request, CompilationLimits::reference_v1())
1063}
1064
1065#[allow(clippy::result_large_err)]
1067pub fn compile_with_limits(
1068 request: PackCompilationRequest,
1069 limits: CompilationLimits,
1070) -> Result<CompilationReport, CompilationRequestRejection> {
1071 let (world, kernel) = match prepare_pack_compilation_with_limits(request, limits) {
1072 PackCompilationPreparation::Execute { world, kernel } => (world, kernel),
1073 PackCompilationPreparation::Report(report) => return Ok(report),
1074 PackCompilationPreparation::Rejected(rejection) => return Err(rejection),
1075 };
1076 Ok(match compile_pack_kernel(world.as_ref(), *kernel) {
1077 PackCompilationKernelOutcome::Execution(execution) => {
1078 let execution = *execution;
1079 CompilationReport {
1080 outcome: CompilationReportOutcome::Result(Box::new(execution.result)),
1081 fulfillments: execution.fulfillments,
1082 }
1083 }
1084 PackCompilationKernelOutcome::Operation(report) => report,
1085 })
1086}
1087
1088pub(crate) struct PreparedPackCompilationKernel {
1089 request: PreparedCompilationRequest,
1090 compilation_identity: CanonicalIdentity,
1091 engine_identity: ImplementationIdentity,
1092 exporter_identity: ImplementationIdentity,
1093 page_selection_implies_untagged_pdf: bool,
1094 fulfillments: CompilationFulfillmentReport,
1095 limits: CompilationLimits,
1096}
1097
1098#[derive(Debug, Clone)]
1099struct PreparedCompilationRequest {
1100 output_specification: CompilationOutputSpecification,
1101 inputs_commitment: u128,
1102 override_commitments: Vec<(String, usize, u128)>,
1103 features: Vec<Feature>,
1104 document_time: DocumentTime,
1105}
1106
1107pub(crate) struct PackCompilationExecution {
1108 pub(crate) result: CompilationResult,
1109 #[cfg(feature = "diagnostics")]
1110 pub(crate) presentation: PackCompilationPresentation,
1111 pub(crate) fulfillments: CompilationFulfillmentReport,
1112}
1113
1114pub(crate) enum PackCompilationKernelOutcome {
1115 Execution(Box<PackCompilationExecution>),
1116 Operation(CompilationReport),
1117}
1118
1119#[cfg(feature = "diagnostics")]
1120pub(crate) enum PackCompilationPresentation {
1121 Succeeded {
1122 warnings: EcoVec<SourceDiagnostic>,
1123 pack_warnings: EcoVec<SourceDiagnostic>,
1124 },
1125 Diagnostics {
1126 errors: EcoVec<SourceDiagnostic>,
1127 warnings: EcoVec<SourceDiagnostic>,
1128 pack_warnings: EcoVec<SourceDiagnostic>,
1129 },
1130 PngExport {
1131 error: String,
1132 warnings: EcoVec<SourceDiagnostic>,
1133 pack_warnings: EcoVec<SourceDiagnostic>,
1134 },
1135}
1136
1137pub(crate) fn prepare_pack_compilation_with_limits(
1138 request: PackCompilationRequest,
1139 limits: CompilationLimits,
1140) -> PackCompilationPreparation {
1141 let PackCompilationRequest {
1142 pack,
1143 output_specification,
1144 inputs,
1145 overrides,
1146 features,
1147 document_time,
1148 fulfillments,
1149 } = request;
1150 let mut output = output_specification;
1151 let mut request_issues = vec![];
1152 let page_selection_implies_untagged_pdf;
1153 match &mut output {
1154 CompilationOutputSpecification::Png(specification) => {
1155 canonicalize_page_selection(&mut specification.page_selection, &mut request_issues);
1156 if specification
1157 .pixels_per_inch
1158 .is_some_and(|ppi| !ppi.is_finite() || ppi <= 0.0)
1159 {
1160 request_issues.push(CompilationRequestIssue::InvalidPpi);
1161 }
1162 if specification.pixels_per_inch.is_none() {
1163 specification.pixels_per_inch = Some(default_png_ppi());
1164 }
1165 page_selection_implies_untagged_pdf = false;
1166 }
1167 CompilationOutputSpecification::Pdf(specification) => {
1168 canonicalize_page_selection(&mut specification.page_selection, &mut request_issues);
1169 specification.standards.sort_by_key(pdf_standard_identity);
1170 if let Err(error) = validate_pdf_standards(&specification.standards) {
1171 request_issues.push(CompilationRequestIssue::InvalidPdfStandards(error));
1172 }
1173 page_selection_implies_untagged_pdf = !specification.page_selection.ranges().is_empty()
1174 && specification.tags.is_auto()
1175 && PdfOptions::default().tagged;
1176 if specification.tags.is_auto() {
1177 specification.tags = Smart::Custom(
1178 PdfOptions::default().tagged
1179 && specification.page_selection.ranges().is_empty(),
1180 );
1181 }
1182 if matches!(
1183 specification.creation_timestamp,
1184 CreationTimestamp::Automatic
1185 ) {
1186 specification.creation_timestamp = CreationTimestamp::Omit;
1187 }
1188 }
1189 CompilationOutputSpecification::Svg(specification) => {
1190 canonicalize_page_selection(&mut specification.page_selection, &mut request_issues);
1191 page_selection_implies_untagged_pdf = false;
1192 }
1193 CompilationOutputSpecification::Html(_) => {
1194 page_selection_implies_untagged_pdf = false;
1195 }
1196 }
1197 if overrides.pack_identity != pack.identity() {
1198 request_issues.push(CompilationRequestIssue::OverrideSetPackMismatch);
1199 }
1200 if features.contains(&Feature::Bundle) {
1201 request_issues.push(CompilationRequestIssue::UnsupportedBundleFeature);
1202 }
1203 if let DocumentTime::UnixTimestamp(timestamp) = document_time
1204 && typst_kit::datetime::Time::fixed_timestamp(timestamp).is_err()
1205 {
1206 request_issues.push(CompilationRequestIssue::InvalidDocumentTimestamp);
1207 }
1208 let derives_html = matches!(&output, CompilationOutputSpecification::Html(_));
1209 let effective_features = [Feature::Html, Feature::Bundle, Feature::A11yExtras]
1210 .into_iter()
1211 .filter(|value| features.contains(value) || (*value == Feature::Html && derives_html))
1212 .collect::<Vec<_>>();
1213 let raw_inputs = inputs;
1214 let total_key_bytes: usize = raw_inputs.iter().map(|(key, _)| key.len()).sum();
1215 let total_value_repr_bytes: usize =
1216 raw_inputs.iter().map(|(_, value)| value.repr().len()).sum();
1217 let inputs_commitment = typst::utils::hash128(&(
1218 "typst-pack-inputs-v1",
1219 total_key_bytes,
1220 total_value_repr_bytes,
1221 &raw_inputs,
1222 ));
1223 let override_commitments = overrides
1224 .replacements
1225 .iter()
1226 .map(|(path, data)| {
1227 (
1228 path.clone(),
1229 data.len(),
1230 typst::utils::hash128(&(
1231 "typst-pack-override-v1+typst-0.15",
1232 "project-file",
1233 overrides.pack_identity.digest_value(),
1234 path,
1235 data.len(),
1236 data,
1237 )),
1238 )
1239 })
1240 .collect();
1241 let prepared_request = PreparedCompilationRequest {
1242 output_specification: output,
1243 inputs_commitment,
1244 override_commitments,
1245 features: effective_features,
1246 document_time,
1247 };
1248 if !request_issues.is_empty() {
1249 return PackCompilationPreparation::Rejected(CompilationRequestRejection::new(
1250 request_issues,
1251 ));
1252 }
1253
1254 let engine_identity = EmbeddedTypst::engine_identity();
1256 let exporter_identity =
1257 EmbeddedTypst::exporter_identity(prepared_request.output_specification.format());
1258 let compilation_identity =
1259 compilation_identity(&pack, &prepared_request, engine_identity, exporter_identity);
1260 let CompilationFulfillmentSet {
1261 packages: package_fulfillments,
1262 fonts: font_fulfillments,
1263 } = fulfillments;
1264 let package_requirements = pack
1265 .package_requirements()
1266 .iter()
1267 .map(|requirement| (requirement.spec().to_string(), requirement))
1268 .collect::<BTreeMap<_, _>>();
1269 let package_report_keys = package_requirements
1270 .keys()
1271 .chain(package_fulfillments.keys())
1272 .cloned()
1273 .collect::<BTreeSet<_>>();
1274 let font_requirements = pack
1275 .font_requirements()
1276 .iter()
1277 .map(|requirement| (requirement.container_identity(), requirement))
1278 .collect::<BTreeMap<_, _>>();
1279 let font_report_keys = font_requirements
1280 .keys()
1281 .chain(font_fulfillments.keys())
1282 .copied()
1283 .collect::<BTreeSet<_>>();
1284 let fulfillments = CompilationFulfillmentReport {
1285 packages: package_report_keys
1286 .into_iter()
1287 .map(|key| {
1288 let requirement = package_requirements.get(&key).copied();
1289 let supplied = package_fulfillments.get(&key);
1290 PackageFulfillmentReport {
1291 spec: requirement
1292 .map(|value| value.spec().clone())
1293 .unwrap_or_else(|| {
1294 supplied
1295 .expect("report key came from a requirement or fulfillment")
1296 .spec
1297 .clone()
1298 }),
1299 required_tree_identity: requirement.map(|value| value.tree_identity()),
1300 supplied_tree_identity: supplied.map(|value| value.tree.identity()),
1301 declared: requirement.is_some(),
1302 embedded: requirement.is_some_and(|value| value.is_embedded()),
1303 provenance: supplied.and_then(|value| value.provenance.clone()),
1304 cache_hit: supplied.is_some_and(|value| value.cache_hit),
1305 }
1306 })
1307 .collect(),
1308 fonts: font_report_keys
1309 .into_iter()
1310 .map(|identity| {
1311 let requirement = font_requirements.get(&identity).copied();
1312 let supplied = font_fulfillments.get(&identity);
1313 FontFulfillmentReport {
1314 container_identity: identity,
1315 supplied_container_identity: supplied.map(|value| value.container.identity()),
1316 declared: requirement.is_some(),
1317 embedded: requirement.is_some_and(|value| value.is_embedded()),
1318 provenance: supplied.and_then(|value| value.provenance.clone()),
1319 licensing: supplied.and_then(|value| value.licensing.clone()),
1320 }
1321 })
1322 .collect(),
1323 };
1324 let fulfillment_issues =
1325 verify_compilation_fulfillment_set(&pack, &package_fulfillments, &font_fulfillments);
1326 if !fulfillment_issues.is_empty() {
1327 return PackCompilationPreparation::Report(CompilationReport {
1328 outcome: CompilationReportOutcome::Operation {
1329 outcome: CompilationOperationOutcome::InvalidFulfillmentSet(
1330 InvalidCompilationFulfillmentSet {
1331 issues: fulfillment_issues,
1332 },
1333 ),
1334 compilation_identity,
1335 },
1336 fulfillments,
1337 });
1338 }
1339 let package_trees = package_fulfillments
1340 .into_iter()
1341 .map(|(spec, fulfillment)| (spec, fulfillment.tree))
1342 .collect();
1343 let font_containers = font_fulfillments
1344 .into_iter()
1345 .map(|(identity, fulfillment)| (identity, fulfillment.container))
1346 .collect();
1347 let dependencies =
1348 pack.materialize_compilation_dependency_snapshot(package_trees, font_containers);
1349
1350 let world = PackWorld::new(
1351 pack,
1352 dependencies,
1353 overrides.replacements,
1354 raw_inputs,
1355 prepared_request.features.clone(),
1356 prepared_request.document_time,
1357 )
1358 .expect("preflighted Pack World inputs must remain valid");
1359
1360 PackCompilationPreparation::Execute {
1361 world: Box::new(world),
1362 kernel: Box::new(PreparedPackCompilationKernel {
1363 request: prepared_request,
1364 compilation_identity,
1365 engine_identity,
1366 exporter_identity,
1367 page_selection_implies_untagged_pdf,
1368 fulfillments,
1369 limits,
1370 }),
1371 }
1372}
1373
1374pub(crate) fn compile_pack_kernel(
1375 world: &PackWorld,
1376 kernel: PreparedPackCompilationKernel,
1377) -> PackCompilationKernelOutcome {
1378 let traced = WorldTrace::new(world);
1379 let compiled = compile_with_default_pdf_timestamp(
1380 &traced,
1381 &kernel.request.output_specification,
1382 kernel.limits,
1383 || None,
1384 );
1385 let access_trace = traced.snapshot();
1386 match compiled {
1387 Ok(output) => {
1388 #[cfg(feature = "diagnostics")]
1389 let warnings = output.warnings.clone();
1390 #[cfg(feature = "diagnostics")]
1391 let mut presentation_pack_warnings = output.pack_warnings.clone();
1392 #[cfg(feature = "diagnostics")]
1393 if kernel.page_selection_implies_untagged_pdf {
1394 presentation_pack_warnings.push(page_selection_pdf_tags_warning());
1395 }
1396 let diagnostics = project_diagnostics(
1397 &traced,
1398 output.warnings,
1399 DiagnosticPhase::Compilation,
1400 DiagnosticProducer::new(kernel.engine_identity),
1401 );
1402 let pack_warnings = project_pack_warnings(
1403 output.pack_warnings,
1404 kernel.page_selection_implies_untagged_pdf,
1405 );
1406 PackCompilationKernelOutcome::Execution(Box::new(PackCompilationExecution {
1407 result: assemble_compilation_result(
1408 &kernel,
1409 CompilationStatus::Succeeded,
1410 output.artifacts,
1411 diagnostics,
1412 pack_warnings,
1413 output.source_page_count,
1414 access_trace,
1415 ),
1416 #[cfg(feature = "diagnostics")]
1417 presentation: PackCompilationPresentation::Succeeded {
1418 warnings,
1419 pack_warnings: presentation_pack_warnings,
1420 },
1421 fulfillments: kernel.fulfillments,
1422 }))
1423 }
1424 Err(CompileError::Diagnostics {
1425 errors,
1426 warnings,
1427 pack_warnings,
1428 phase,
1429 source_page_count,
1430 }) => {
1431 #[cfg(feature = "diagnostics")]
1432 let mut presentation_pack_warnings = pack_warnings.clone();
1433 #[cfg(feature = "diagnostics")]
1434 if kernel.page_selection_implies_untagged_pdf {
1435 presentation_pack_warnings.push(page_selection_pdf_tags_warning());
1436 }
1437 #[cfg(feature = "diagnostics")]
1438 let presentation = PackCompilationPresentation::Diagnostics {
1439 errors: errors.clone(),
1440 warnings: warnings.clone(),
1441 pack_warnings: presentation_pack_warnings,
1442 };
1443 let mut diagnostics = project_diagnostics(
1444 &traced,
1445 warnings,
1446 DiagnosticPhase::Compilation,
1447 DiagnosticProducer::new(kernel.engine_identity),
1448 );
1449 let producer = match phase {
1450 DiagnosticPhase::Compilation => DiagnosticProducer::new(kernel.engine_identity),
1451 DiagnosticPhase::Export => DiagnosticProducer::new(kernel.exporter_identity),
1452 };
1453 diagnostics.extend(project_diagnostics(&traced, errors, phase, producer));
1454 PackCompilationKernelOutcome::Execution(Box::new(PackCompilationExecution {
1455 result: assemble_compilation_result(
1456 &kernel,
1457 CompilationStatus::Rejected,
1458 vec![],
1459 diagnostics,
1460 project_pack_warnings(
1461 pack_warnings,
1462 kernel.page_selection_implies_untagged_pdf,
1463 ),
1464 source_page_count,
1465 access_trace,
1466 ),
1467 #[cfg(feature = "diagnostics")]
1468 presentation,
1469 fulfillments: kernel.fulfillments,
1470 }))
1471 }
1472 Err(CompileError::PngExport {
1473 message,
1474 warnings,
1475 pack_warnings,
1476 source_page_count,
1477 source_page_number,
1478 }) => {
1479 #[cfg(feature = "diagnostics")]
1480 let mut presentation_pack_warnings = pack_warnings.clone();
1481 #[cfg(feature = "diagnostics")]
1482 if kernel.page_selection_implies_untagged_pdf {
1483 presentation_pack_warnings.push(page_selection_pdf_tags_warning());
1484 }
1485 #[cfg(feature = "diagnostics")]
1486 let presentation = PackCompilationPresentation::PngExport {
1487 error: format!("PNG export failed for source page {source_page_number}: {message}"),
1488 warnings: warnings.clone(),
1489 pack_warnings: presentation_pack_warnings,
1490 };
1491 let mut diagnostics = project_diagnostics(
1492 &traced,
1493 warnings,
1494 DiagnosticPhase::Compilation,
1495 DiagnosticProducer::new(kernel.engine_identity),
1496 );
1497 diagnostics.push(CompilationDiagnostic {
1498 severity: DiagnosticSeverity::Error,
1499 message,
1500 span: LogicalSpan {
1501 logical_path: None,
1502 byte_range: None,
1503 },
1504 hints: vec![],
1505 trace: vec![],
1506 phase: DiagnosticPhase::Export,
1507 producer: DiagnosticProducer::new(kernel.exporter_identity),
1508 source_page_number: Some(source_page_number),
1509 });
1510 PackCompilationKernelOutcome::Execution(Box::new(PackCompilationExecution {
1511 result: assemble_compilation_result(
1512 &kernel,
1513 CompilationStatus::Rejected,
1514 vec![],
1515 diagnostics,
1516 project_pack_warnings(
1517 pack_warnings,
1518 kernel.page_selection_implies_untagged_pdf,
1519 ),
1520 Some(source_page_count),
1521 access_trace,
1522 ),
1523 #[cfg(feature = "diagnostics")]
1524 presentation,
1525 fulfillments: kernel.fulfillments,
1526 }))
1527 }
1528 Err(CompileError::InvalidPdfStandards(error)) => {
1529 unreachable!("PDF standards are validated during request preparation: {error}");
1530 }
1531 Err(CompileError::Limit(error)) => {
1532 PackCompilationKernelOutcome::Operation(CompilationReport {
1533 outcome: CompilationReportOutcome::Operation {
1534 outcome: CompilationOperationOutcome::ResourceLimit(error),
1535 compilation_identity: kernel.compilation_identity,
1536 },
1537 fulfillments: kernel.fulfillments,
1538 })
1539 }
1540 }
1541}
1542
1543fn assemble_compilation_result(
1544 kernel: &PreparedPackCompilationKernel,
1545 status: CompilationStatus,
1546 artifacts: Vec<CompilationArtifact>,
1547 diagnostics: Vec<CompilationDiagnostic>,
1548 pack_warnings: Vec<PackCompilationWarning>,
1549 source_page_count: Option<usize>,
1550 access_trace: CompilationAccessTrace,
1551) -> CompilationResult {
1552 finalize_result(CompilationResult {
1553 status,
1554 artifacts,
1555 diagnostics,
1556 pack_warnings,
1557 document: document_summary(&kernel.request.output_specification, source_page_count),
1558 access_trace,
1559 result_identity: CanonicalIdentity::from_digest(
1560 CanonicalIdentityRole::CompilationResult,
1561 0,
1562 ),
1563 compilation_identity: kernel.compilation_identity,
1564 engine_identity: kernel.engine_identity,
1565 exporter_identity: kernel.exporter_identity,
1566 })
1567}
1568
1569fn document_summary(
1570 output: &CompilationOutputSpecification,
1571 source_page_count: Option<usize>,
1572) -> CompilationDocumentSummary {
1573 CompilationDocumentSummary {
1574 target: output.target(),
1575 source_page_count,
1576 }
1577}
1578
1579fn finalize_result(mut result: CompilationResult) -> CompilationResult {
1580 let artifacts = result
1581 .artifacts
1582 .iter()
1583 .map(|artifact| {
1584 (
1585 artifact.format,
1586 artifact.source_page_number,
1587 artifact.bytes.len(),
1588 typst::utils::hash128(artifact.bytes.as_slice()),
1589 )
1590 })
1591 .collect::<Vec<_>>();
1592 result.result_identity = CanonicalIdentity::from_digest(
1593 CanonicalIdentityRole::CompilationResult,
1594 typst::utils::hash128(&(
1595 "typst-pack-compilation-result-v1",
1596 result.compilation_identity,
1597 result.status,
1598 result.document,
1599 &result.diagnostics,
1600 &result.pack_warnings,
1601 &result.access_trace,
1602 artifacts,
1603 )),
1604 );
1605 result
1606}
1607
1608fn compilation_identity(
1609 pack: &Pack,
1610 request: &PreparedCompilationRequest,
1611 engine_identity: ImplementationIdentity,
1612 exporter_identity: ImplementationIdentity,
1613) -> CanonicalIdentity {
1614 let output_digest = match &request.output_specification {
1615 CompilationOutputSpecification::Pdf(specification) => {
1616 let page_selection = canonical_page_selection(&specification.page_selection);
1617 let mut standards = specification
1618 .standards
1619 .iter()
1620 .map(pdf_standard_identity)
1621 .collect::<Vec<_>>();
1622 standards.sort_unstable();
1623 typst::utils::hash128(&(
1624 "pdf",
1625 &page_selection,
1626 &specification.identifier,
1627 &specification.creator,
1628 specification.tags,
1629 specification.creation_timestamp,
1630 standards,
1631 specification.pretty,
1632 ))
1633 }
1634 CompilationOutputSpecification::Png(specification) => {
1635 let page_selection = canonical_page_selection(&specification.page_selection);
1636 typst::utils::hash128(&(
1637 "png",
1638 &page_selection,
1639 specification.pixels_per_inch.map(f64::to_bits),
1640 specification.render_bleed,
1641 ))
1642 }
1643 CompilationOutputSpecification::Svg(specification) => {
1644 let page_selection = canonical_page_selection(&specification.page_selection);
1645 typst::utils::hash128(&(
1646 "svg",
1647 &page_selection,
1648 specification.render_bleed,
1649 specification.pretty,
1650 ))
1651 }
1652 CompilationOutputSpecification::Html(specification) => {
1653 typst::utils::hash128(&("html", specification.pretty))
1654 }
1655 };
1656 let (document_time, document_timestamp) = request.document_time.identity_projection();
1657 let override_commitments = request
1658 .override_commitments
1659 .iter()
1660 .map(|(path, byte_len, commitment)| (path, *byte_len, *commitment))
1661 .collect::<Vec<_>>();
1662 let projection = (
1663 "typst-pack-compilation-v1",
1664 pack.identity(),
1665 request.output_specification.format(),
1666 output_digest,
1667 request.inputs_commitment,
1668 override_commitments,
1669 &request.features,
1670 document_time,
1671 document_timestamp,
1672 engine_identity,
1673 exporter_identity,
1674 );
1675 CanonicalIdentity::from_digest(
1676 CanonicalIdentityRole::Compilation,
1677 typst::utils::hash128(&projection),
1678 )
1679}
1680
1681fn canonical_page_selection(selection: &PageSelection) -> (bool, Vec<(usize, usize)>) {
1682 let selects_all = selection.ranges.is_empty();
1683 let mut ranges = selection
1684 .ranges
1685 .iter()
1686 .filter_map(|range| {
1687 let start = range.start().map_or(1, NonZeroUsize::get);
1688 let end = range.end().map_or(usize::MAX, NonZeroUsize::get);
1689 (start <= end).then_some((start, end))
1690 })
1691 .collect::<Vec<_>>();
1692 ranges.sort_unstable();
1693 let mut canonical: Vec<(usize, usize)> = vec![];
1694 for (start, end) in ranges {
1695 if let Some(last) = canonical.last_mut()
1696 && start <= last.1.saturating_add(1)
1697 {
1698 last.1 = last.1.max(end);
1699 } else {
1700 canonical.push((start, end));
1701 }
1702 }
1703 (selects_all, canonical)
1704}
1705
1706fn canonicalize_page_selection(
1707 selection: &mut PageSelection,
1708 issues: &mut Vec<CompilationRequestIssue>,
1709) {
1710 let invalid = selection
1711 .ranges
1712 .iter()
1713 .filter_map(|range| {
1714 let (Some(start), Some(end)) = (*range.start(), *range.end()) else {
1715 return None;
1716 };
1717 (start > end).then_some((start, end))
1718 })
1719 .collect::<BTreeSet<_>>();
1720 issues.extend(
1721 invalid
1722 .iter()
1723 .map(|(start, end)| CompilationRequestIssue::InvalidPageRange {
1724 start: *start,
1725 end: *end,
1726 }),
1727 );
1728 if selection.ranges.is_empty() {
1729 return;
1730 }
1731 let (_, ranges) = canonical_page_selection(selection);
1732 let mut ranges = ranges
1733 .into_iter()
1734 .map(|(start, end)| {
1735 let start = Some(NonZeroUsize::new(start).expect("canonical page starts at one"));
1736 let end = (end != usize::MAX)
1737 .then(|| NonZeroUsize::new(end).expect("canonical page ends at one or later"));
1738 start..=end
1739 })
1740 .chain(
1741 invalid
1742 .into_iter()
1743 .map(|(start, end)| Some(start)..=Some(end)),
1744 )
1745 .collect::<Vec<_>>();
1746 ranges.sort_by_key(|range| {
1747 (
1748 range.start().map_or(1, NonZeroUsize::get),
1749 range.end().map_or(usize::MAX, NonZeroUsize::get),
1750 )
1751 });
1752 selection.ranges = ranges;
1753}
1754
1755fn pdf_standard_identity(standard: &PdfStandard) -> &'static str {
1756 match standard {
1757 PdfStandard::V_1_4 => "1.4",
1758 PdfStandard::V_1_5 => "1.5",
1759 PdfStandard::V_1_6 => "1.6",
1760 PdfStandard::V_1_7 => "1.7",
1761 PdfStandard::V_2_0 => "2.0",
1762 PdfStandard::A_1b => "a-1b",
1763 PdfStandard::A_1a => "a-1a",
1764 PdfStandard::A_2b => "a-2b",
1765 PdfStandard::A_2u => "a-2u",
1766 PdfStandard::A_2a => "a-2a",
1767 PdfStandard::A_3b => "a-3b",
1768 PdfStandard::A_3u => "a-3u",
1769 PdfStandard::A_3a => "a-3a",
1770 PdfStandard::A_4 => "a-4",
1771 PdfStandard::A_4f => "a-4f",
1772 PdfStandard::A_4e => "a-4e",
1773 PdfStandard::Ua_1 => "ua-1",
1774 _ => unreachable!("all standards in pinned typst-pdf are represented"),
1775 }
1776}
1777
1778pub(crate) fn compile_with_default_pdf_timestamp(
1779 world: &dyn World,
1780 specification: &CompilationOutputSpecification,
1781 limits: CompilationLimits,
1782 default_pdf_timestamp: impl FnOnce() -> Option<Timestamp>,
1783) -> Result<CompilationOutput, CompileError> {
1784 let _compilation_timing = typst_timing::TimingScope::new("typst-pack compilation");
1785 if let CompilationOutputSpecification::Html(specification) = specification {
1786 let pack_warnings = EcoVec::new();
1787 let Warned { output, warnings } = EmbeddedTypst::compile_html(world);
1788 let document = output.map_err(|errors| CompileError::Diagnostics {
1789 errors,
1790 warnings: warnings.clone(),
1791 pack_warnings: pack_warnings.clone(),
1792 phase: DiagnosticPhase::Compilation,
1793 source_page_count: None,
1794 })?;
1795 check_artifact_count(limits, 1)?;
1796 let _export_timing = typst_timing::TimingScope::new("export");
1797 let bytes = EmbeddedTypst::export_html(
1798 &document,
1799 &typst_html::HtmlOptions {
1800 pretty: specification.pretty,
1801 },
1802 )
1803 .map_err(|errors| CompileError::Diagnostics {
1804 errors,
1805 warnings: warnings.clone(),
1806 pack_warnings: pack_warnings.clone(),
1807 phase: DiagnosticPhase::Export,
1808 source_page_count: None,
1809 })?;
1810 let artifacts = vec![CompilationArtifact {
1811 format: OutputFormat::Html,
1812 bytes: SharedBytes::new(bytes),
1813 source_page_number: None,
1814 }];
1815 check_artifact_bytes(limits, &artifacts)?;
1816 return Ok(CompilationOutput {
1817 artifacts,
1818 warnings,
1819 pack_warnings,
1820 source_page_count: None,
1821 });
1822 }
1823
1824 let Warned {
1825 output,
1826 warnings: compile_warnings,
1827 } = EmbeddedTypst::compile_paged(world);
1828 let warnings = compile_warnings;
1829 let mut pack_warnings = EcoVec::new();
1830 if let CompilationOutputSpecification::Pdf(specification) = specification
1831 && !specification.page_selection.ranges().is_empty()
1832 && specification.tags.is_auto()
1833 && PdfOptions::default().tagged
1834 {
1835 pack_warnings.push(page_selection_pdf_tags_warning());
1836 }
1837 let document = output.map_err(|errors| CompileError::Diagnostics {
1838 errors,
1839 warnings: warnings.clone(),
1840 pack_warnings: pack_warnings.clone(),
1841 phase: DiagnosticPhase::Compilation,
1842 source_page_count: None,
1843 })?;
1844 let source_page_count = document.pages().len();
1845 check_compilation_limit(
1846 CompilationResource::SourcePages,
1847 limits.source_pages(),
1848 u64::try_from(source_page_count).map_err(|_| {
1849 CompilationLimitError::AccountingOverflow {
1850 resource: CompilationResource::SourcePages,
1851 }
1852 })?,
1853 )?;
1854 let artifacts = {
1855 let _export_timing = typst_timing::TimingScope::new("export");
1856 match specification {
1857 CompilationOutputSpecification::Pdf(specification) => {
1858 check_artifact_count(limits, 1)?;
1859 let standards = validate_pdf_standards(&specification.standards)
1860 .map_err(CompileError::InvalidPdfStandards)?;
1861 let timestamp = match specification.creation_timestamp {
1862 CreationTimestamp::Automatic => default_pdf_timestamp(),
1863 CreationTimestamp::Explicit(timestamp) => Some(timestamp),
1864 CreationTimestamp::Omit => None,
1865 };
1866 let pdf_options = PdfOptions {
1867 ident: specification.identifier.clone(),
1868 creator: specification.creator.clone(),
1869 timestamp,
1870 page_ranges: specification.page_selection.typst_page_ranges(),
1871 standards,
1872 tagged: match specification.tags {
1873 Smart::Auto => {
1874 PdfOptions::default().tagged
1875 && specification.page_selection.ranges().is_empty()
1876 }
1877 Smart::Custom(tagged) => tagged,
1878 },
1879 pretty: specification.pretty,
1880 };
1881 let pdf = EmbeddedTypst::export_pdf(&document, &pdf_options).map_err(|errors| {
1882 CompileError::Diagnostics {
1883 errors,
1884 warnings: warnings.clone(),
1885 pack_warnings: pack_warnings.clone(),
1886 phase: DiagnosticPhase::Export,
1887 source_page_count: Some(source_page_count),
1888 }
1889 })?;
1890 vec![CompilationArtifact {
1891 format: OutputFormat::Pdf,
1892 bytes: SharedBytes::new(pdf),
1893 source_page_number: None,
1894 }]
1895 }
1896 CompilationOutputSpecification::Png(specification) => {
1897 let pixels_per_inch = specification
1898 .pixels_per_inch
1899 .unwrap_or_else(default_png_ppi);
1900 let render_options = typst_render::RenderOptions {
1901 pixel_per_pt: (pixels_per_inch / 72.0).into(),
1902 render_bleed: specification.render_bleed,
1903 };
1904 let pages =
1905 selected_pages(&document, &specification.page_selection).collect::<Vec<_>>();
1906 check_artifact_count(limits, pages.len())?;
1907 check_png_pixels(limits, &pages, &render_options)?;
1908 let export = |(source_page_number, page)| {
1909 let bytes =
1910 EmbeddedTypst::export_png(page, &render_options).map_err(|message| {
1911 CompileError::PngExport {
1912 message,
1913 warnings: warnings.clone(),
1914 pack_warnings: pack_warnings.clone(),
1915 source_page_count,
1916 source_page_number,
1917 }
1918 })?;
1919 Ok::<_, CompileError>(CompilationArtifact {
1920 format: OutputFormat::Png,
1921 bytes: SharedBytes::new(bytes),
1922 source_page_number: Some(source_page_number),
1923 })
1924 };
1925 export_artifacts_bounded(pages, limits, export)?
1926 }
1927 CompilationOutputSpecification::Svg(specification) => {
1928 let svg_options = typst_svg::SvgOptions {
1929 render_bleed: specification.render_bleed,
1930 pretty: specification.pretty,
1931 };
1932 let pages =
1933 selected_pages(&document, &specification.page_selection).collect::<Vec<_>>();
1934 check_artifact_count(limits, pages.len())?;
1935 let export = |(source_page_number, page)| {
1936 Ok(CompilationArtifact {
1937 format: OutputFormat::Svg,
1938 bytes: SharedBytes::new(EmbeddedTypst::export_svg(page, &svg_options)),
1939 source_page_number: Some(source_page_number),
1940 })
1941 };
1942 export_artifacts_bounded(pages, limits, export)?
1943 }
1944 CompilationOutputSpecification::Html(_) => unreachable!("handled above"),
1945 }
1946 };
1947 check_artifact_bytes(limits, &artifacts)?;
1948 Ok(CompilationOutput {
1949 artifacts,
1950 warnings,
1951 pack_warnings,
1952 source_page_count: Some(source_page_count),
1953 })
1954}
1955
1956fn check_compilation_limit(
1957 resource: CompilationResource,
1958 ceiling: u64,
1959 observed: u64,
1960) -> Result<(), CompilationLimitError> {
1961 if observed > ceiling {
1962 Err(CompilationLimitError::exceeded(resource, ceiling))
1963 } else {
1964 Ok(())
1965 }
1966}
1967
1968fn check_artifact_count(
1969 limits: CompilationLimits,
1970 count: usize,
1971) -> Result<(), CompilationLimitError> {
1972 let observed = u64::try_from(count).map_err(|_| CompilationLimitError::AccountingOverflow {
1973 resource: CompilationResource::Artifacts,
1974 })?;
1975 check_compilation_limit(CompilationResource::Artifacts, limits.artifacts(), observed)
1976}
1977
1978fn check_png_pixels(
1979 limits: CompilationLimits,
1980 pages: &[(NonZeroUsize, &typst_layout::Page)],
1981 options: &typst_render::RenderOptions,
1982) -> Result<(), CompilationLimitError> {
1983 let mut total = 0u64;
1984 for (_, page) in pages {
1985 let size = if options.render_bleed {
1986 page.frame.size() + page.bleed.sum_by_axis()
1987 } else {
1988 page.frame.size()
1989 };
1990 let pixel_per_pt = options.pixel_per_pt.get() as f32;
1991 let width = (pixel_per_pt * size.x.to_pt() as f32).round().max(1.0) as u32;
1992 let height = (pixel_per_pt * size.y.to_pt() as f32).round().max(1.0) as u32;
1993 let pixels = u64::from(width).checked_mul(u64::from(height)).ok_or(
1994 CompilationLimitError::AccountingOverflow {
1995 resource: CompilationResource::PixelsPerArtifact,
1996 },
1997 )?;
1998 check_compilation_limit(
1999 CompilationResource::PixelsPerArtifact,
2000 limits.pixels_per_artifact(),
2001 pixels,
2002 )?;
2003 total = total
2004 .checked_add(pixels)
2005 .ok_or(CompilationLimitError::AccountingOverflow {
2006 resource: CompilationResource::TotalPixels,
2007 })?;
2008 }
2009 check_compilation_limit(
2010 CompilationResource::TotalPixels,
2011 limits.total_pixels(),
2012 total,
2013 )
2014}
2015
2016fn check_artifact_bytes(
2017 limits: CompilationLimits,
2018 artifacts: &[CompilationArtifact],
2019) -> Result<(), CompilationLimitError> {
2020 let mut retained = 0u64;
2021 for artifact in artifacts {
2022 retain_artifact_bytes(limits, &mut retained, artifact)?;
2023 }
2024 Ok(())
2025}
2026
2027fn retain_artifact_bytes(
2028 limits: CompilationLimits,
2029 retained: &mut u64,
2030 artifact: &CompilationArtifact,
2031) -> Result<(), CompilationLimitError> {
2032 let bytes = u64::try_from(artifact.bytes.len()).map_err(|_| {
2033 CompilationLimitError::AccountingOverflow {
2034 resource: CompilationResource::ArtifactBytes,
2035 }
2036 })?;
2037 check_compilation_limit(
2038 CompilationResource::ArtifactBytes,
2039 limits.artifact_bytes(),
2040 bytes,
2041 )?;
2042 *retained = retained
2043 .checked_add(bytes)
2044 .ok_or(CompilationLimitError::AccountingOverflow {
2045 resource: CompilationResource::RetainedArtifactBytes,
2046 })?;
2047 check_compilation_limit(
2048 CompilationResource::RetainedArtifactBytes,
2049 limits.retained_artifact_bytes(),
2050 *retained,
2051 )
2052}
2053
2054pub(crate) fn validate_pdf_standards(
2055 standards: &[PdfStandard],
2056) -> Result<PdfStandards, PdfStandardsValidationError> {
2057 PdfStandards::new(standards).map_err(|error| PdfStandardsValidationError {
2058 message: error.message().to_string(),
2059 hints: error.hints().iter().map(ToString::to_string).collect(),
2060 })
2061}
2062
2063#[cfg(feature = "diagnostics")]
2064pub(crate) fn pdf_standard_requiring_tags(standards: &[PdfStandard]) -> Option<&'static str> {
2065 standards.iter().find_map(|standard| match standard {
2066 PdfStandard::A_1a => Some("PDF/A-1a"),
2067 PdfStandard::A_2a => Some("PDF/A-2a"),
2068 PdfStandard::A_3a => Some("PDF/A-3a"),
2069 PdfStandard::Ua_1 => Some("PDF/UA-1"),
2070 _ => None,
2071 })
2072}
2073
2074fn selected_pages<'a>(
2075 document: &'a PagedDocument,
2076 page_selection: &'a PageSelection,
2077) -> impl Iterator<Item = (NonZeroUsize, &'a typst_layout::Page)> {
2078 let ranges = page_selection.typst_page_ranges();
2079 document
2080 .pages()
2081 .iter()
2082 .enumerate()
2083 .filter(move |(index, _)| {
2084 ranges.as_ref().is_none_or(|ranges| {
2085 NonZeroUsize::new(index + 1).is_some_and(|number| ranges.includes_page(number))
2086 })
2087 })
2088 .map(|(index, page)| (NonZeroUsize::new(index + 1).unwrap(), page))
2089}
2090
2091#[cfg(feature = "parallel")]
2092fn export_artifacts_bounded<T>(
2093 items: Vec<T>,
2094 limits: CompilationLimits,
2095 export: impl Fn(T) -> Result<CompilationArtifact, CompileError> + Sync + Send,
2096) -> Result<Vec<CompilationArtifact>, CompileError>
2097where
2098 T: Send,
2099{
2100 if items.is_empty() {
2101 return Ok(vec![]);
2102 }
2103 let workers = usize::try_from(limits.export_workers())
2104 .unwrap_or(usize::MAX)
2105 .min(items.len());
2106 let pool = rayon::ThreadPoolBuilder::new()
2107 .num_threads(workers)
2108 .build()
2109 .ok();
2110 let mut items = items.into_iter();
2111 let mut artifacts = Vec::new();
2112 let mut retained = 0;
2113 loop {
2114 let batch = items.by_ref().take(workers).collect::<Vec<_>>();
2115 if batch.is_empty() {
2116 break;
2117 }
2118 let batch: Vec<Result<CompilationArtifact, CompileError>> = match &pool {
2119 Some(pool) => pool.install(|| batch.into_par_iter().map(&export).collect()),
2120 None => batch.into_iter().map(&export).collect(),
2121 };
2122 for artifact in batch {
2123 let artifact = artifact?;
2124 retain_artifact_bytes(limits, &mut retained, &artifact)?;
2125 artifacts.push(artifact);
2126 }
2127 }
2128 Ok(artifacts)
2129}
2130
2131#[cfg(not(feature = "parallel"))]
2132fn export_artifacts_bounded<T>(
2133 items: Vec<T>,
2134 limits: CompilationLimits,
2135 export: impl Fn(T) -> Result<CompilationArtifact, CompileError>,
2136) -> Result<Vec<CompilationArtifact>, CompileError> {
2137 let mut artifacts = Vec::new();
2138 let mut retained = 0;
2139 for item in items {
2140 let artifact = export(item)?;
2141 retain_artifact_bytes(limits, &mut retained, &artifact)?;
2142 artifacts.push(artifact);
2143 }
2144 Ok(artifacts)
2145}
2146
2147fn default_png_ppi() -> f64 {
2148 typst_render::RenderOptions::default().pixel_per_pt.get() * 72.0
2149}
2150
2151fn project_diagnostics(
2152 world: &dyn World,
2153 diagnostics: impl IntoIterator<Item = SourceDiagnostic>,
2154 phase: DiagnosticPhase,
2155 producer: DiagnosticProducer,
2156) -> Vec<CompilationDiagnostic> {
2157 diagnostics
2158 .into_iter()
2159 .map(|diagnostic| CompilationDiagnostic {
2160 severity: match diagnostic.severity {
2161 Severity::Error => DiagnosticSeverity::Error,
2162 Severity::Warning => DiagnosticSeverity::Warning,
2163 },
2164 message: diagnostic.message.into(),
2165 span: logical_span(world, diagnostic.span),
2166 hints: diagnostic
2167 .hints
2168 .into_iter()
2169 .map(|hint| DiagnosticHint {
2170 message: hint.v.into(),
2171 span: logical_span(world, hint.span),
2172 })
2173 .collect(),
2174 trace: diagnostic
2175 .trace
2176 .into_iter()
2177 .map(|trace| {
2178 let (kind, value) = match trace.v {
2179 Tracepoint::Call(value) => (TracepointKind::Call, value.map(String::from)),
2180 Tracepoint::Show(value) => (TracepointKind::Show, Some(value.into())),
2181 Tracepoint::Import(value) => (TracepointKind::Import, Some(value.into())),
2182 Tracepoint::Include(value) => (TracepointKind::Include, Some(value.into())),
2183 };
2184 DiagnosticTracepoint {
2185 kind,
2186 value,
2187 span: logical_span(world, trace.span.into()),
2188 }
2189 })
2190 .collect(),
2191 phase,
2192 producer,
2193 source_page_number: None,
2194 })
2195 .collect()
2196}
2197
2198fn project_pack_warnings(
2199 warnings: impl IntoIterator<Item = SourceDiagnostic>,
2200 page_selection_implies_untagged_pdf: bool,
2201) -> Vec<PackCompilationWarning> {
2202 warnings
2203 .into_iter()
2204 .chain(page_selection_implies_untagged_pdf.then(page_selection_pdf_tags_warning))
2205 .map(|warning| PackCompilationWarning {
2206 message: warning.message.into(),
2207 hints: warning
2208 .hints
2209 .into_iter()
2210 .map(|hint| hint.v.into())
2211 .collect(),
2212 })
2213 .collect()
2214}
2215
2216fn page_selection_pdf_tags_warning() -> SourceDiagnostic {
2217 SourceDiagnostic::warning(Span::detached(), "using --pages implies --no-pdf-tags").with_hints([
2218 "the resulting PDF will be inaccessible".into(),
2219 "add --no-pdf-tags to silence this warning".into(),
2220 ])
2221}
2222
2223fn logical_span(world: &dyn World, span: DiagSpan) -> LogicalSpan {
2224 LogicalSpan {
2225 logical_path: span.id().map(logical_path),
2226 byte_range: world.range(span),
2227 }
2228}
2229
2230#[cfg(test)]
2231mod result_identity_tests {
2232 use super::*;
2233
2234 #[test]
2235 fn compilation_trace_retains_missing_font_requests() {
2236 let trace = CompilationAccessTrace::from_observations(BTreeSet::from([
2237 CompilationAccessObservation::new(
2238 CompilationAccessKind::Font,
2239 "font-index:7".to_owned(),
2240 Some(7),
2241 CompilationAccessOutcome::Missing,
2242 ),
2243 ]));
2244
2245 let observation = trace.observations().next().unwrap();
2246 assert_eq!(observation.kind(), CompilationAccessKind::Font);
2247 assert_eq!(observation.logical_path(), "font-index:7");
2248 assert_eq!(observation.font_index(), Some(7));
2249 assert_eq!(observation.outcome(), &CompilationAccessOutcome::Missing);
2250 }
2251
2252 #[test]
2253 fn result_identity_binds_each_post_execution_projection() {
2254 let pack = Pack::builder("main.typ")
2255 .file(
2256 "main.typ",
2257 b"#set page(width: 20pt, height: 10pt, margin: 0pt)\n#rect(width: 1pt, height: 1pt)".to_vec(),
2258 )
2259 .unwrap()
2260 .build()
2261 .unwrap();
2262 let report = compile_with_limits(
2263 PackCompilationRequest::new(
2264 pack,
2265 CompilationOutputSpecification::Svg(SvgOutputSpecification::default()),
2266 ),
2267 CompilationLimits::reference_v1(),
2268 )
2269 .unwrap();
2270 let base = report.result().unwrap().clone();
2271 let identity = base.result_identity;
2272
2273 let mut compilation = base.clone();
2274 compilation.compilation_identity = CanonicalIdentity::from_digest(
2275 CanonicalIdentityRole::Compilation,
2276 base.compilation_identity.digest_value() ^ 1,
2277 );
2278 assert_ne!(finalize_result(compilation).result_identity, identity);
2279
2280 let mut status = base.clone();
2281 status.status = CompilationStatus::Rejected;
2282 assert_ne!(finalize_result(status).result_identity, identity);
2283
2284 let mut target = base.clone();
2285 target.document.target = TypstTarget::Html;
2286 assert_ne!(finalize_result(target).result_identity, identity);
2287
2288 let mut document = base.clone();
2289 document.document.source_page_count = Some(2);
2290 assert_ne!(finalize_result(document).result_identity, identity);
2291
2292 let diagnostic = CompilationDiagnostic {
2293 severity: DiagnosticSeverity::Warning,
2294 message: "identity warning".to_owned(),
2295 span: LogicalSpan {
2296 logical_path: Some("project:main.typ".to_owned()),
2297 byte_range: Some(1..2),
2298 },
2299 hints: vec![DiagnosticHint {
2300 message: "identity hint".to_owned(),
2301 span: LogicalSpan {
2302 logical_path: Some("project:hint.typ".to_owned()),
2303 byte_range: Some(2..3),
2304 },
2305 }],
2306 trace: vec![DiagnosticTracepoint {
2307 kind: TracepointKind::Call,
2308 value: Some("identity trace".to_owned()),
2309 span: LogicalSpan {
2310 logical_path: Some("project:trace.typ".to_owned()),
2311 byte_range: Some(3..4),
2312 },
2313 }],
2314 phase: DiagnosticPhase::Compilation,
2315 producer: DiagnosticProducer::new(base.engine_identity),
2316 source_page_number: NonZeroUsize::new(1),
2317 };
2318 let mut diagnostics = base.clone();
2319 diagnostics.diagnostics.push(diagnostic.clone());
2320 let diagnostic_identity = finalize_result(diagnostics).result_identity;
2321 assert_ne!(diagnostic_identity, identity);
2322 let diagnostic_mutations = [
2323 CompilationDiagnostic {
2324 severity: DiagnosticSeverity::Error,
2325 ..diagnostic.clone()
2326 },
2327 CompilationDiagnostic {
2328 message: "changed warning".to_owned(),
2329 ..diagnostic.clone()
2330 },
2331 CompilationDiagnostic {
2332 span: LogicalSpan {
2333 logical_path: Some("project:changed.typ".to_owned()),
2334 ..diagnostic.span.clone()
2335 },
2336 ..diagnostic.clone()
2337 },
2338 CompilationDiagnostic {
2339 span: LogicalSpan {
2340 byte_range: Some(4..5),
2341 ..diagnostic.span.clone()
2342 },
2343 ..diagnostic.clone()
2344 },
2345 CompilationDiagnostic {
2346 hints: vec![DiagnosticHint {
2347 message: "changed hint".to_owned(),
2348 ..diagnostic.hints[0].clone()
2349 }],
2350 ..diagnostic.clone()
2351 },
2352 CompilationDiagnostic {
2353 hints: vec![DiagnosticHint {
2354 span: LogicalSpan {
2355 logical_path: Some("project:changed-hint.typ".to_owned()),
2356 ..diagnostic.hints[0].span.clone()
2357 },
2358 ..diagnostic.hints[0].clone()
2359 }],
2360 ..diagnostic.clone()
2361 },
2362 CompilationDiagnostic {
2363 hints: vec![DiagnosticHint {
2364 span: LogicalSpan {
2365 byte_range: Some(5..6),
2366 ..diagnostic.hints[0].span.clone()
2367 },
2368 ..diagnostic.hints[0].clone()
2369 }],
2370 ..diagnostic.clone()
2371 },
2372 CompilationDiagnostic {
2373 trace: vec![DiagnosticTracepoint {
2374 kind: TracepointKind::Include,
2375 ..diagnostic.trace[0].clone()
2376 }],
2377 ..diagnostic.clone()
2378 },
2379 CompilationDiagnostic {
2380 trace: vec![DiagnosticTracepoint {
2381 value: Some("changed trace".to_owned()),
2382 ..diagnostic.trace[0].clone()
2383 }],
2384 ..diagnostic.clone()
2385 },
2386 CompilationDiagnostic {
2387 trace: vec![DiagnosticTracepoint {
2388 span: LogicalSpan {
2389 logical_path: Some("project:changed-trace.typ".to_owned()),
2390 ..diagnostic.trace[0].span.clone()
2391 },
2392 ..diagnostic.trace[0].clone()
2393 }],
2394 ..diagnostic.clone()
2395 },
2396 CompilationDiagnostic {
2397 trace: vec![DiagnosticTracepoint {
2398 span: LogicalSpan {
2399 byte_range: Some(6..7),
2400 ..diagnostic.trace[0].span.clone()
2401 },
2402 ..diagnostic.trace[0].clone()
2403 }],
2404 ..diagnostic.clone()
2405 },
2406 CompilationDiagnostic {
2407 phase: DiagnosticPhase::Export,
2408 ..diagnostic.clone()
2409 },
2410 CompilationDiagnostic {
2411 producer: DiagnosticProducer::new(base.exporter_identity),
2412 ..diagnostic.clone()
2413 },
2414 CompilationDiagnostic {
2415 source_page_number: NonZeroUsize::new(2),
2416 ..diagnostic
2417 },
2418 ];
2419 for diagnostic in &diagnostic_mutations {
2420 let mut result = base.clone();
2421 result.diagnostics.push(diagnostic.clone());
2422 assert_ne!(finalize_result(result).result_identity, diagnostic_identity);
2423 }
2424
2425 let mut warning = base.clone();
2426 warning.pack_warnings.push(PackCompilationWarning {
2427 message: "identity warning".to_owned(),
2428 hints: vec!["identity hint".to_owned()],
2429 });
2430 let warning_identity = finalize_result(warning).result_identity;
2431 assert_ne!(warning_identity, identity);
2432 for warning in [
2433 PackCompilationWarning {
2434 message: "changed warning".to_owned(),
2435 hints: vec!["identity hint".to_owned()],
2436 },
2437 PackCompilationWarning {
2438 message: "identity warning".to_owned(),
2439 hints: vec!["changed hint".to_owned()],
2440 },
2441 ] {
2442 let mut result = base.clone();
2443 result.pack_warnings.push(warning);
2444 assert_ne!(finalize_result(result).result_identity, warning_identity);
2445 }
2446
2447 let observation = CompilationAccessObservation {
2448 kind: CompilationAccessKind::File,
2449 logical_path: "project:identity.txt".to_owned(),
2450 font_index: Some(1),
2451 outcome: CompilationAccessOutcome::Read {
2452 byte_length: 8,
2453 digest: [1; 16],
2454 },
2455 };
2456 let mut access = base.clone();
2457 access.access_trace.observations.insert(observation.clone());
2458 let access_identity = finalize_result(access).result_identity;
2459 assert_ne!(access_identity, identity);
2460 let observation_mutations = [
2461 CompilationAccessObservation {
2462 kind: CompilationAccessKind::Source,
2463 ..observation.clone()
2464 },
2465 CompilationAccessObservation {
2466 logical_path: "project:changed.txt".to_owned(),
2467 ..observation.clone()
2468 },
2469 CompilationAccessObservation {
2470 font_index: Some(2),
2471 ..observation.clone()
2472 },
2473 CompilationAccessObservation {
2474 outcome: CompilationAccessOutcome::Read {
2475 byte_length: 9,
2476 digest: [1; 16],
2477 },
2478 ..observation.clone()
2479 },
2480 CompilationAccessObservation {
2481 outcome: CompilationAccessOutcome::Read {
2482 byte_length: 8,
2483 digest: [2; 16],
2484 },
2485 ..observation.clone()
2486 },
2487 CompilationAccessObservation {
2488 outcome: CompilationAccessOutcome::Missing,
2489 ..observation.clone()
2490 },
2491 CompilationAccessObservation {
2492 outcome: CompilationAccessOutcome::Failed,
2493 ..observation
2494 },
2495 ];
2496 for observation in observation_mutations {
2497 let mut result = base.clone();
2498 result.access_trace.observations.insert(observation);
2499 assert_ne!(finalize_result(result).result_identity, access_identity);
2500 }
2501
2502 let mut artifact_format = base.clone();
2503 artifact_format.artifacts[0].format = OutputFormat::Png;
2504 assert_ne!(finalize_result(artifact_format).result_identity, identity);
2505
2506 let mut artifact_page = base.clone();
2507 artifact_page.artifacts[0].source_page_number = NonZeroUsize::new(2);
2508 assert_ne!(finalize_result(artifact_page).result_identity, identity);
2509
2510 let mut artifact = base.clone();
2511 let mut artifact_bytes = artifact.artifacts[0].bytes.as_slice().to_vec();
2512 artifact_bytes.push(0);
2513 artifact.artifacts[0].bytes = SharedBytes::new(artifact_bytes);
2514 assert_ne!(finalize_result(artifact).result_identity, identity);
2515
2516 let mut ordered = base.clone();
2517 let mut second = ordered.artifacts[0].clone();
2518 let mut second_bytes = second.bytes.as_slice().to_vec();
2519 second_bytes.push(0);
2520 second.bytes = SharedBytes::new(second_bytes);
2521 ordered.artifacts.push(second);
2522 let ordered_identity = finalize_result(ordered.clone()).result_identity;
2523 ordered.artifacts.reverse();
2524 assert_ne!(finalize_result(ordered).result_identity, ordered_identity);
2525
2526 let mut ordered_diagnostics = base.clone();
2527 let mut first_diagnostic = diagnostic_mutations[0].clone();
2528 first_diagnostic.message = "first diagnostic".to_owned();
2529 let mut second_diagnostic = diagnostic_mutations[1].clone();
2530 second_diagnostic.message = "second diagnostic".to_owned();
2531 ordered_diagnostics.diagnostics = vec![first_diagnostic, second_diagnostic];
2532 let ordered_diagnostics_identity =
2533 finalize_result(ordered_diagnostics.clone()).result_identity;
2534 ordered_diagnostics.diagnostics.reverse();
2535 assert_ne!(
2536 finalize_result(ordered_diagnostics).result_identity,
2537 ordered_diagnostics_identity
2538 );
2539
2540 let mut ordered_warnings = base;
2541 ordered_warnings.pack_warnings = vec![
2542 PackCompilationWarning {
2543 message: "first warning".to_owned(),
2544 hints: vec![],
2545 },
2546 PackCompilationWarning {
2547 message: "second warning".to_owned(),
2548 hints: vec![],
2549 },
2550 ];
2551 let ordered_warnings_identity = finalize_result(ordered_warnings.clone()).result_identity;
2552 ordered_warnings.pack_warnings.reverse();
2553 assert_ne!(
2554 finalize_result(ordered_warnings).result_identity,
2555 ordered_warnings_identity
2556 );
2557 }
2558
2559 #[test]
2560 fn compilation_identity_binds_every_implementation_identity_field() {
2561 fn mutations(
2562 identity: ImplementationIdentity,
2563 implementation: &'static str,
2564 ) -> [ImplementationIdentity; 7] {
2565 [
2566 ImplementationIdentity {
2567 implementation,
2568 ..identity
2569 },
2570 ImplementationIdentity {
2571 version: "changed-version",
2572 ..identity
2573 },
2574 ImplementationIdentity {
2575 source_checksum: "changed-checksum",
2576 ..identity
2577 },
2578 ImplementationIdentity {
2579 target: "changed-target",
2580 ..identity
2581 },
2582 ImplementationIdentity {
2583 target_features: "changed-target-features",
2584 ..identity
2585 },
2586 ImplementationIdentity {
2587 feature_set: "changed-feature-set",
2588 ..identity
2589 },
2590 ImplementationIdentity {
2591 debug_assertions: !identity.debug_assertions,
2592 ..identity
2593 },
2594 ]
2595 }
2596
2597 let pack = Pack::builder("main.typ")
2598 .file("main.typ", b"implementation identity".to_vec())
2599 .unwrap()
2600 .build()
2601 .unwrap();
2602 let PackCompilationPreparation::Execute { kernel, .. } =
2603 prepare_pack_compilation_with_limits(
2604 PackCompilationRequest::new(
2605 pack.clone(),
2606 CompilationOutputSpecification::Svg(SvgOutputSpecification::default()),
2607 ),
2608 CompilationLimits::reference_v1(),
2609 )
2610 else {
2611 panic!("valid request must be prepared for execution");
2612 };
2613 let engine = kernel.engine_identity;
2614 let exporter = kernel.exporter_identity;
2615 let baseline = kernel.compilation_identity;
2616 for engine in mutations(engine, "changed-engine") {
2617 assert_ne!(
2618 compilation_identity(&pack, &kernel.request, engine, exporter),
2619 baseline
2620 );
2621 }
2622
2623 for exporter in mutations(exporter, "changed-exporter") {
2624 assert_ne!(
2625 compilation_identity(&pack, &kernel.request, engine, exporter),
2626 baseline
2627 );
2628 }
2629 }
2630
2631 #[cfg(feature = "parallel")]
2632 #[test]
2633 fn parallel_export_scheduler_obeys_worker_limit_and_preserves_input_order() {
2634 use std::sync::atomic::{AtomicUsize, Ordering};
2635 use std::sync::{Arc, Barrier};
2636
2637 for demand in [3u8, 4, 5] {
2638 let expected_peak = usize::from(demand.min(4));
2639 let active = Arc::new(AtomicUsize::new(0));
2640 let peak = Arc::new(AtomicUsize::new(0));
2641 let barrier = Arc::new(Barrier::new(expected_peak));
2642 let output = export_artifacts_bounded(
2643 (0u8..demand).collect(),
2644 CompilationLimits::reference_v1(),
2645 {
2646 let active = Arc::clone(&active);
2647 let peak = Arc::clone(&peak);
2648 let barrier = Arc::clone(&barrier);
2649 move |item| {
2650 let now = active.fetch_add(1, Ordering::SeqCst) + 1;
2651 peak.fetch_max(now, Ordering::SeqCst);
2652 if usize::from(item) < expected_peak {
2653 barrier.wait();
2654 }
2655 let workers = rayon::current_num_threads();
2656 active.fetch_sub(1, Ordering::SeqCst);
2657 Ok(CompilationArtifact {
2658 format: OutputFormat::Svg,
2659 bytes: SharedBytes::new(vec![item, workers as u8]),
2660 source_page_number: None,
2661 })
2662 }
2663 },
2664 )
2665 .unwrap();
2666
2667 assert_eq!(peak.load(Ordering::SeqCst), expected_peak);
2668 assert_eq!(
2669 output
2670 .iter()
2671 .map(|artifact| artifact.bytes().to_vec())
2672 .collect::<Vec<_>>(),
2673 (0u8..demand)
2674 .map(|item| vec![item, expected_peak as u8])
2675 .collect::<Vec<_>>()
2676 );
2677 }
2678 }
2679
2680 #[cfg(feature = "parallel")]
2681 #[test]
2682 fn worker_count_does_not_change_exporter_and_limit_outcome_order() {
2683 let execute = |workers| {
2684 export_artifacts_bounded(
2685 vec![0u8, 1],
2686 CompilationLimits::new(2, 2, 1, 1, 0, 2, workers),
2687 |item| {
2688 if item == 1 {
2689 Err(CompileError::PngExport {
2690 message: "later exporter failure".to_owned(),
2691 warnings: EcoVec::new(),
2692 pack_warnings: EcoVec::new(),
2693 source_page_count: 2,
2694 source_page_number: NonZeroUsize::new(2).unwrap(),
2695 })
2696 } else {
2697 Ok(CompilationArtifact {
2698 format: OutputFormat::Png,
2699 bytes: SharedBytes::new(vec![item]),
2700 source_page_number: NonZeroUsize::new(1),
2701 })
2702 }
2703 },
2704 )
2705 .unwrap_err()
2706 };
2707
2708 for error in [execute(1), execute(2)] {
2709 assert!(matches!(
2710 error,
2711 CompileError::Limit(CompilationLimitError::Exceeded {
2712 resource: CompilationResource::ArtifactBytes,
2713 ceiling: 0,
2714 observed_at_least: 1,
2715 })
2716 ));
2717 }
2718 }
2719}