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