tinymist_world/
compute.rs

1use std::any::TypeId;
2use std::borrow::Cow;
3use std::sync::{Arc, OnceLock};
4
5use parking_lot::Mutex;
6use tinymist_std::error::prelude::*;
7use tinymist_std::typst::{TypstHtmlDocument, TypstPagedDocument};
8use typst::diag::{At, SourceResult, Warned};
9use typst::ecow::EcoVec;
10use typst::foundations::Output;
11use typst::syntax::Span;
12use typst_bundle::Bundle;
13
14use crate::snapshot::CompileSnapshot;
15use crate::{CompilerFeat, CompilerWorld, EntryReader, TaskInputs};
16
17type AnyArc = Arc<dyn std::any::Any + Send + Sync>;
18
19/// A world compute entry.
20#[derive(Debug, Clone, Default)]
21struct WorldComputeEntry {
22    computed: Arc<OnceLock<Result<AnyArc>>>,
23}
24
25impl WorldComputeEntry {
26    fn cast<T: std::any::Any + Send + Sync>(e: Result<AnyArc>) -> Result<Arc<T>> {
27        e.map(|e| e.downcast().expect("T is T"))
28    }
29}
30
31/// A world compute graph.
32pub struct WorldComputeGraph<F: CompilerFeat> {
33    /// The used snapshot.
34    pub snap: CompileSnapshot<F>,
35    /// The computed entries.
36    entries: Mutex<rpds::RedBlackTreeMapSync<TypeId, WorldComputeEntry>>,
37}
38
39/// A world computable trait.
40pub trait WorldComputable<F: CompilerFeat>: std::any::Any + Send + Sync + Sized {
41    /// The output type.
42    type Output: Send + Sync + 'static;
43
44    /// The computation implementation.
45    ///
46    /// ## Example
47    ///
48    /// The example shows that a computation can depend on specific world
49    /// implementation. It computes the system font that only works on the
50    /// system world.
51    ///
52    /// ```rust
53    /// use std::sync::Arc;
54    ///
55    /// use tinymist_std::error::prelude::*;
56    /// use tinymist_world::{WorldComputeGraph, WorldComputable};
57    /// use tinymist_world::font::FontResolverImpl;
58    /// use tinymist_world::system::SystemCompilerFeat;
59    ///
60    ///
61    /// pub struct SystemFontsOnce {
62    ///     fonts: Arc<FontResolverImpl>,
63    /// }
64    ///
65    /// impl WorldComputable<SystemCompilerFeat> for SystemFontsOnce {
66    ///     type Output = Self;
67    ///
68    ///     fn compute(graph: &Arc<WorldComputeGraph<SystemCompilerFeat>>) -> Result<Self> {
69    ///
70    ///         Ok(Self {
71    ///             fonts: graph.snap.world.font_resolver.clone(),
72    ///         })
73    ///     }
74    /// }
75    ///
76    /// /// Computes the system fonts.
77    /// fn compute_system_fonts(graph: &Arc<WorldComputeGraph<SystemCompilerFeat>>) {
78    ///    let _fonts = graph.compute::<SystemFontsOnce>().expect("font").fonts.clone();
79    /// }
80    /// ```
81    fn compute(graph: &Arc<WorldComputeGraph<F>>) -> Result<Self::Output>;
82}
83
84impl<F: CompilerFeat> WorldComputeGraph<F> {
85    /// Creates a new world compute graph.
86    pub fn new(snap: CompileSnapshot<F>) -> Arc<Self> {
87        Arc::new(Self {
88            snap,
89            entries: Default::default(),
90        })
91    }
92
93    /// Creates a graph from the world.
94    pub fn from_world(world: CompilerWorld<F>) -> Arc<Self> {
95        Self::new(CompileSnapshot::from_world(world))
96    }
97
98    /// Clones the graph with the same snapshot.
99    pub fn snapshot(&self) -> Arc<Self> {
100        self.snapshot_unsafe(self.snap.clone())
101    }
102
103    /// Clones the graph with the same snapshot. Take care of the consistency by
104    /// your self.
105    pub fn snapshot_unsafe(&self, snap: CompileSnapshot<F>) -> Arc<Self> {
106        Arc::new(Self {
107            snap,
108            entries: Mutex::new(self.entries.lock().clone()),
109        })
110    }
111
112    /// Forks a new snapshot that compiles a different document.
113    // todo: share cache if task doesn't change.
114    pub fn task(&self, inputs: TaskInputs) -> Arc<Self> {
115        let mut snap = self.snap.clone();
116        snap = snap.task(inputs);
117        Self::new(snap)
118    }
119
120    /// Gets a world computed.
121    pub fn must_get<T: WorldComputable<F>>(&self) -> Result<Arc<T::Output>> {
122        let res = self.get::<T>().transpose()?;
123        res.with_context("computation not found", || {
124            Some(Box::new([("type", std::any::type_name::<T>().to_owned())]))
125        })
126    }
127
128    /// Gets a world computed.
129    pub fn get<T: WorldComputable<F>>(&self) -> Option<Result<Arc<T::Output>>> {
130        let computed = self.computed(TypeId::of::<T>()).computed;
131        computed.get().cloned().map(WorldComputeEntry::cast)
132    }
133
134    /// Provides an exact instance.
135    pub fn exact_provide<T: WorldComputable<F>>(&self, ins: Result<Arc<T::Output>>) {
136        if self.provide::<T>(ins).is_err() {
137            panic!(
138                "failed to provide computed instance: {:?}",
139                std::any::type_name::<T>()
140            );
141        }
142    }
143
144    /// Provides some precomputed instance.
145    #[must_use = "the result must be checked"]
146    pub fn provide<T: WorldComputable<F>>(
147        &self,
148        ins: Result<Arc<T::Output>>,
149    ) -> Result<(), Result<Arc<T::Output>>> {
150        let entry = self.computed(TypeId::of::<T>()).computed;
151        let initialized = entry.set(ins.map(|e| e as AnyArc));
152        initialized.map_err(WorldComputeEntry::cast)
153    }
154
155    /// Gets or computes a world computable.
156    pub fn compute<T: WorldComputable<F>>(self: &Arc<Self>) -> Result<Arc<T::Output>> {
157        let entry = self.computed(TypeId::of::<T>()).computed;
158        let computed = entry.get_or_init(|| Ok(Arc::new(T::compute(self)?)));
159        WorldComputeEntry::cast(computed.clone())
160    }
161
162    fn computed(&self, id: TypeId) -> WorldComputeEntry {
163        let mut entries = self.entries.lock();
164        if let Some(entry) = entries.get(&id) {
165            entry.clone()
166        } else {
167            let entry = WorldComputeEntry::default();
168            entries.insert_mut(id, entry.clone());
169            entry
170        }
171    }
172
173    /// Gets the world.
174    pub fn world(&self) -> &CompilerWorld<F> {
175        &self.snap.world
176    }
177
178    /// Gets the registry.
179    pub fn registry(&self) -> &Arc<F::Registry> {
180        &self.snap.world.registry
181    }
182
183    /// Gets the library.
184    pub fn library(&self) -> &typst::Library {
185        &self.snap.world.library
186    }
187}
188
189/// A trait to detect the export of a document.
190pub trait ExportDetection<F: CompilerFeat, D> {
191    /// The configuration type.
192    type Config: Send + Sync + 'static;
193
194    /// Determines whether the export needs to be computed.
195    fn needs_run(graph: &Arc<WorldComputeGraph<F>>, config: &Self::Config) -> bool;
196}
197
198/// A trait to compute the export of a document.
199pub trait ExportComputation<F: CompilerFeat, D> {
200    /// The output type.
201    type Output;
202    /// The configuration type.
203    type Config: Send + Sync + 'static;
204
205    /// Runs the export computation.
206    fn run_with<C: WorldComputable<F, Output = Option<Arc<D>>>>(
207        g: &Arc<WorldComputeGraph<F>>,
208        config: &Self::Config,
209    ) -> Result<Self::Output> {
210        let doc = g.compute::<C>()?;
211        let doc = doc.as_ref().as_ref().context("document not found")?;
212        Self::run(g, doc, config)
213    }
214
215    /// Runs the export computation with a caster.
216    fn cast_run<'a>(
217        g: &Arc<WorldComputeGraph<F>>,
218        doc: impl TryInto<&'a Arc<D>, Error = tinymist_std::Error>,
219        config: &Self::Config,
220    ) -> Result<Self::Output>
221    where
222        D: 'a,
223    {
224        Self::run(g, doc.try_into()?, config)
225    }
226
227    /// Runs the export computation.
228    fn run(
229        g: &Arc<WorldComputeGraph<F>>,
230        doc: &Arc<D>,
231        config: &Self::Config,
232    ) -> Result<Self::Output>;
233}
234
235/// A task that computes a configuration supplied by an external caller.
236pub struct ConfigTask<T>(pub T);
237
238impl<F: CompilerFeat, T: Send + Sync + 'static> WorldComputable<F> for ConfigTask<T> {
239    type Output = T;
240
241    fn compute(_graph: &Arc<WorldComputeGraph<F>>) -> Result<T> {
242        let id = std::any::type_name::<T>();
243        panic!("{id:?} must be provided before computation");
244    }
245}
246
247/// A task that compiles a paged document.
248pub type PagedCompilationTask = CompilationTask<TypstPagedDocument>;
249
250/// A task that compiles an HTML document.
251pub type HtmlCompilationTask = CompilationTask<TypstHtmlDocument>;
252
253/// A task that compiles a bundle.
254pub type BundleCompilationTask = CompilationTask<Bundle>;
255
256/// A task that compiles a document.
257pub struct CompilationTask<D>(std::marker::PhantomData<D>);
258
259impl<D: Output + Send + Sync + 'static> CompilationTask<D> {
260    /// Ensures the main document.
261    pub fn ensure_main<F: CompilerFeat>(world: &CompilerWorld<F>) -> SourceResult<()> {
262        let main_id = world.main_id();
263        let checked = main_id.ok_or_else(|| typst::diag::eco_format!("entry file is not set"));
264        checked.at(Span::detached()).map(|_| ())
265    }
266
267    /// Executes the compilation.
268    pub fn execute<F: CompilerFeat>(world: &CompilerWorld<F>) -> Warned<SourceResult<Arc<D>>> {
269        let res = Self::ensure_main(world);
270        if let Err(err) = res {
271            return Warned {
272                output: Err(err),
273                warnings: EcoVec::new(),
274            };
275        }
276
277        let is_paged_compilation = TypeId::of::<D>() == TypeId::of::<TypstPagedDocument>();
278        let is_html_compilation = TypeId::of::<D>() == TypeId::of::<TypstHtmlDocument>();
279
280        let mut world = if is_paged_compilation {
281            world.paged_task()
282        } else if is_html_compilation {
283            // todo: create html world once
284            world.html_task()
285        } else {
286            Cow::Borrowed(world)
287        };
288
289        world.to_mut().set_is_compiling(true);
290        let compiled = ::typst_shim::compile_opt::<D>(world.as_ref());
291        world.to_mut().set_is_compiling(false);
292
293        let exclude_html_warnings = if !is_html_compilation {
294            compiled.warnings
295        } else if compiled.warnings.len() == 1
296            && compiled.warnings[0]
297                .message
298                .starts_with("html export is under active development")
299        {
300            EcoVec::new()
301        } else {
302            compiled.warnings
303        };
304
305        Warned {
306            output: compiled.output.map(Arc::new),
307            warnings: exclude_html_warnings,
308        }
309    }
310}
311
312impl<F: CompilerFeat, D> WorldComputable<F> for CompilationTask<D>
313where
314    D: Output + Send + Sync + 'static,
315{
316    type Output = Warned<SourceResult<Arc<D>>>;
317
318    fn compute(graph: &Arc<WorldComputeGraph<F>>) -> Result<Self::Output> {
319        Ok(CompilationTask::<D>::execute(&graph.snap.world))
320    }
321}
322
323/// A task that computes an optional document.
324pub struct OptionDocumentTask<D>(std::marker::PhantomData<D>);
325
326impl<F: CompilerFeat, D> WorldComputable<F> for OptionDocumentTask<D>
327where
328    D: Output + Send + Sync + 'static,
329{
330    type Output = Option<Arc<D>>;
331
332    fn compute(graph: &Arc<WorldComputeGraph<F>>) -> Result<Self::Output> {
333        let compiled = graph.compute::<CompilationTask<D>>()?;
334        let compiled = compiled.output.clone().ok();
335
336        Ok(compiled)
337    }
338}
339
340impl<D> OptionDocumentTask<D> where D: Output + Send + Sync + 'static {}
341
342/// A task that computes the diagnostics of a document.
343struct CompilationDiagnostics {
344    errors: Option<EcoVec<typst::diag::SourceDiagnostic>>,
345    warnings: Option<EcoVec<typst::diag::SourceDiagnostic>>,
346}
347
348impl CompilationDiagnostics {
349    /// Creates a new diagnostics from a result.
350    fn from_result<T>(result: Option<&Warned<SourceResult<T>>>) -> Self {
351        let errors = result.and_then(|r| r.output.as_ref().map_err(|e| e.clone()).err());
352        let warnings = result.map(|r| r.warnings.clone());
353
354        Self { errors, warnings }
355    }
356}
357
358/// A task that computes the diagnostics of a document.
359pub struct DiagnosticsTask {
360    paged: CompilationDiagnostics,
361    html: CompilationDiagnostics,
362    bundle: CompilationDiagnostics,
363}
364
365impl DiagnosticsTask {
366    /// Collects diagnostics from compilation tasks that have already been
367    /// requested from the graph.
368    fn collect<F: CompilerFeat>(graph: &WorldComputeGraph<F>) -> Result<Self> {
369        let paged = graph.get::<PagedCompilationTask>().transpose()?;
370        let html = graph.get::<HtmlCompilationTask>().transpose()?;
371        let bundle = graph.get::<BundleCompilationTask>().transpose()?;
372
373        Ok(Self {
374            paged: CompilationDiagnostics::from_result(paged.as_deref()),
375            html: CompilationDiagnostics::from_result(html.as_deref()),
376            bundle: CompilationDiagnostics::from_result(bundle.as_deref()),
377        })
378    }
379
380    /// Creates diagnostics from errors.
381    pub fn from_errors(paged_errors: Option<EcoVec<typst::diag::SourceDiagnostic>>) -> Self {
382        Self {
383            paged: CompilationDiagnostics {
384                errors: paged_errors,
385                warnings: None,
386            },
387            html: CompilationDiagnostics {
388                errors: None,
389                warnings: None,
390            },
391            bundle: CompilationDiagnostics {
392                errors: None,
393                warnings: None,
394            },
395        }
396    }
397
398    /// Gets the number of errors.
399    pub fn error_cnt(&self) -> usize {
400        self.paged.errors.as_ref().map_or(0, |e| e.len())
401            + self.html.errors.as_ref().map_or(0, |e| e.len())
402            + self.bundle.errors.as_ref().map_or(0, |e| e.len())
403    }
404
405    /// Gets the number of warnings.
406    pub fn warning_cnt(&self) -> usize {
407        self.paged.warnings.as_ref().map_or(0, |e| e.len())
408            + self.html.warnings.as_ref().map_or(0, |e| e.len())
409            + self.bundle.warnings.as_ref().map_or(0, |e| e.len())
410    }
411
412    /// Gets the diagnostics.
413    pub fn diagnostics(&self) -> impl Iterator<Item = &typst::diag::SourceDiagnostic> + Clone {
414        self.paged
415            .errors
416            .iter()
417            .chain(self.paged.warnings.iter())
418            .chain(self.html.errors.iter())
419            .chain(self.html.warnings.iter())
420            .chain(self.bundle.errors.iter())
421            .chain(self.bundle.warnings.iter())
422            .flatten()
423    }
424}
425
426impl<F: CompilerFeat> WorldComputeGraph<F> {
427    /// Ensures the main document.
428    pub fn ensure_main(&self) -> SourceResult<()> {
429        CompilationTask::<TypstPagedDocument>::ensure_main(&self.snap.world)
430    }
431
432    /// Compiles once from scratch.
433    pub fn pure_compile<D: ::typst::foundations::Output + Send + Sync + 'static>(
434        &self,
435    ) -> Warned<SourceResult<Arc<D>>> {
436        CompilationTask::<D>::execute(&self.snap.world)
437    }
438
439    /// Compiles once from scratch.
440    pub fn compile(&self) -> Warned<SourceResult<Arc<TypstPagedDocument>>> {
441        self.pure_compile()
442    }
443
444    /// Compiles to html once from scratch.
445    pub fn compile_html(&self) -> Warned<SourceResult<Arc<TypstHtmlDocument>>> {
446        self.pure_compile()
447    }
448
449    /// Compiles paged document with cache
450    pub fn shared_compile(self: &Arc<Self>) -> Result<Option<Arc<TypstPagedDocument>>> {
451        let doc = self.compute::<OptionDocumentTask<TypstPagedDocument>>()?;
452        Ok(doc.as_ref().clone())
453    }
454
455    /// Compiles HTML document with cache
456    pub fn shared_compile_html(self: &Arc<Self>) -> Result<Option<Arc<TypstHtmlDocument>>> {
457        let doc = self.compute::<OptionDocumentTask<TypstHtmlDocument>>()?;
458        Ok(doc.as_ref().clone())
459    }
460
461    /// Gets the diagnostics from shared compilation.
462    #[must_use = "the result must be checked"]
463    pub fn shared_diagnostics(self: &Arc<Self>) -> Result<Arc<DiagnosticsTask>> {
464        Ok(Arc::new(DiagnosticsTask::collect(self)?))
465    }
466}