Skip to main content

typst_library/
engine.rs

1//! Definition of the central compilation context.
2
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use comemo::{Track, Tracked, TrackedMut};
6use ecow::EcoVec;
7use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
8use rustc_hash::FxHashSet;
9use typst_syntax::{FileId, Span};
10use typst_utils::{LazyHash, Protected};
11
12use crate::diag::{HintedStrResult, SourceDiagnostic, SourceResult, StrResult, bail};
13use crate::foundations::{Styles, Value};
14use crate::introspection::{Introspect, Introspection, Introspector};
15use crate::{Library, World};
16
17/// Holds all data needed during compilation.
18pub struct Engine<'a> {
19    /// The compilation environment.
20    pub world: Tracked<'a, dyn World + 'a>,
21    /// Definition of Typst's standard library.
22    ///
23    /// Can be accessed via `world.library()`, but we fetch it once upfront
24    /// because it's accessed so frequently and we want to avoid the overhead of
25    /// the tracked call.
26    pub library: &'a LazyHash<Library>,
27    /// Provides access to information about the document.
28    pub introspector: Protected<Tracked<'a, dyn Introspector + 'a>>,
29    /// May hold a span that is currently under inspection.
30    pub traced: Tracked<'a, Traced>,
31    /// A pure sink for warnings, delayed errors, and spans under inspection.
32    pub sink: TrackedMut<'a, Sink>,
33    /// The route the engine took during compilation. This is used to detect
34    /// cyclic imports and excessive nesting.
35    pub route: Route<'a>,
36}
37
38impl Engine<'_> {
39    /// Handles a result without immediately terminating execution. Instead, it
40    /// produces a delayed error that is only promoted to a fatal one if it
41    /// remains by the end of the introspection loop.
42    pub fn delay<T: Default>(&mut self, result: SourceResult<T>) -> T {
43        match result {
44            Ok(value) => value,
45            Err(errors) => {
46                self.sink.delayed_errors(errors);
47                T::default()
48            }
49        }
50    }
51
52    /// Runs tasks on the engine in parallel.
53    pub fn parallelize<P, I, T, U, F>(
54        &mut self,
55        iter: P,
56        f: F,
57    ) -> impl Iterator<Item = U> + use<P, I, T, U, F>
58    where
59        P: IntoIterator<IntoIter = I>,
60        I: Iterator<Item = T>,
61        T: Send,
62        U: Send,
63        F: Fn(&mut Engine, T) -> U + Send + Sync,
64    {
65        let Engine {
66            world, introspector, traced, ref route, library, ..
67        } = *self;
68
69        // We collect into a vector and then call `into_par_iter` instead of
70        // using `par_bridge` because it does not retain the ordering.
71        let work: Vec<T> = iter.into_iter().collect();
72
73        // Work in parallel.
74        let mut pairs: Vec<(U, Sink)> = Vec::with_capacity(work.len());
75        work.into_par_iter()
76            .map(|value| {
77                let mut sink = Sink::new();
78                let mut engine = Engine {
79                    world,
80                    introspector,
81                    traced,
82                    sink: sink.track_mut(),
83                    route: route.clone(),
84                    library,
85                };
86                (f(&mut engine, value), sink)
87            })
88            .collect_into_vec(&mut pairs);
89
90        // Apply the subsinks to the outer sink.
91        for (_, sink) in &mut pairs {
92            let sink = std::mem::take(sink);
93            self.sink.extend(
94                sink.introspections,
95                sink.delayed,
96                sink.warnings,
97                sink.values,
98            );
99        }
100
101        pairs.into_iter().map(|(output, _)| output)
102    }
103
104    /// Performs an introspection on the introspector and returns its result.
105    ///
106    /// As a side effect, the introspection is stored in the sink. If the
107    /// document does not converge, the recorded introspections are used to
108    /// determine the cause of non-convergence.
109    pub fn introspect<I>(&mut self, introspection: I) -> I::Output
110    where
111        I: Introspect,
112    {
113        let introspector = *self.introspector.access("is okay since we're recording it");
114        let output = introspection.introspect(self, introspector);
115        self.sink.introspection(Introspection::new(introspection));
116        output
117    }
118}
119
120/// May hold a span that is currently under inspection.
121#[derive(Default)]
122pub struct Traced(Option<Span>);
123
124impl Traced {
125    /// Wraps a to-be-traced `Span`.
126    ///
127    /// Call `Traced::default()` to trace nothing.
128    pub fn new(traced: Span) -> Self {
129        Self(Some(traced))
130    }
131}
132
133#[comemo::track]
134impl Traced {
135    /// Returns the traced span _if_ it is part of the given source file or
136    /// `None` otherwise.
137    ///
138    /// We hide the span if it isn't in the given file so that only results for
139    /// the file with the traced span are invalidated.
140    pub fn get(&self, id: FileId) -> Option<Span> {
141        if self.0.and_then(Span::id) == Some(id) { self.0 } else { None }
142    }
143}
144
145/// A push-only sink for recorded introspections, delayed errors, warnings, and
146/// traced values.
147///
148/// All tracked methods of this type are of the form `(&mut self, ..) -> ()`, so
149/// in principle they do not need validation (though that optimization is not
150/// yet implemented in comemo).
151#[derive(Default, Clone)]
152pub struct Sink {
153    /// Introspections that were performed during compilation.
154    introspections: EcoVec<Introspection>,
155    /// Delayed errors: Those are errors that we can ignore until the last
156    /// iteration. For instance, show rules may throw during earlier iterations
157    /// because the introspector is not yet ready. We first ignore that and
158    /// proceed with empty content and only if the error remains by the end
159    /// of the last iteration, we promote it.
160    delayed: EcoVec<SourceDiagnostic>,
161    /// Warnings emitted during iteration.
162    warnings: EcoVec<SourceDiagnostic>,
163    /// Hashes of all warning's spans and messages for warning deduplication.
164    warnings_set: FxHashSet<u128>,
165    /// A sequence of traced values for a span.
166    values: EcoVec<(Value, Option<Styles>)>,
167}
168
169impl Sink {
170    /// The maximum number of traced values.
171    pub const MAX_VALUES: usize = 10;
172
173    /// Create a new empty sink.
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Get the introspections.
179    pub fn introspections(&self) -> &[Introspection] {
180        &self.introspections
181    }
182
183    /// Get the stored delayed errors.
184    pub fn delayed(&mut self) -> EcoVec<SourceDiagnostic> {
185        std::mem::take(&mut self.delayed)
186    }
187
188    /// Get the stored warnings.
189    pub fn warnings(self) -> EcoVec<SourceDiagnostic> {
190        self.warnings
191    }
192
193    /// Get the values for the traced span.
194    pub fn values(self) -> EcoVec<(Value, Option<Styles>)> {
195        self.values
196    }
197
198    /// Extend from another sink.
199    pub fn extend_from_sink(&mut self, other: Sink) {
200        self.extend(other.introspections, other.delayed, other.warnings, other.values);
201    }
202}
203
204#[comemo::track]
205impl Sink {
206    /// Trace an introspection.
207    pub fn introspection(&mut self, introspection: Introspection) {
208        self.introspections.push(introspection);
209    }
210
211    /// Add a delayed error.
212    pub fn delayed_error(&mut self, error: SourceDiagnostic) {
213        self.delayed.push(error);
214    }
215
216    /// Add multiple delayed errors.
217    pub fn delayed_errors(&mut self, errors: EcoVec<SourceDiagnostic>) {
218        self.delayed.extend(errors);
219    }
220
221    /// Add a warning.
222    pub fn warn(&mut self, warning: SourceDiagnostic) {
223        // Check if warning is a duplicate.
224        let hash = typst_utils::hash128(&(&warning.span, &warning.message));
225        if self.warnings_set.insert(hash) {
226            self.warnings.push(warning);
227        }
228    }
229
230    /// Trace a value and optionally styles for the traced span.
231    pub fn value(&mut self, value: Value, styles: Option<Styles>) {
232        if self.values.len() < Self::MAX_VALUES {
233            self.values.push((value, styles));
234        }
235    }
236
237    /// Extend from parts of another sink.
238    fn extend(
239        &mut self,
240        introspections: EcoVec<Introspection>,
241        delayed: EcoVec<SourceDiagnostic>,
242        warnings: EcoVec<SourceDiagnostic>,
243        values: EcoVec<(Value, Option<Styles>)>,
244    ) {
245        self.introspections.extend(introspections);
246        self.delayed.extend(delayed);
247        for warning in warnings {
248            self.warn(warning);
249        }
250        if let Some(remaining) = Self::MAX_VALUES.checked_sub(self.values.len()) {
251            self.values.extend(values.into_iter().take(remaining));
252        }
253    }
254}
255
256/// The route the engine took during compilation. This is used to detect
257/// cyclic imports and excessive nesting.
258pub struct Route<'a> {
259    /// The parent route segment, if present.
260    ///
261    /// This is used when an engine is created from another engine.
262    // We need to override the constraint's lifetime here so that `Tracked` is
263    // covariant over the constraint. If it becomes invariant, we're in for a
264    // world of lifetime pain.
265    outer: Option<Tracked<'a, Self, <Route<'static> as Track>::Call>>,
266    /// This is set if this route segment was inserted through the start of a
267    /// module evaluation.
268    id: Option<FileId>,
269    /// This is set whenever we enter a function, nested layout, or are applying
270    /// a show rule. The length of this segment plus the lengths of all `outer`
271    /// route segments make up the length of the route. If the length of the
272    /// route exceeds `MAX_DEPTH`, then we throw a "maximum ... depth exceeded"
273    /// error.
274    len: usize,
275    /// The upper bound we've established for the parent chain length.
276    ///
277    /// We don't know the exact length (that would defeat the whole purpose
278    /// because it would prevent cache reuse of some computation at different,
279    /// non-exceeding depths).
280    upper: AtomicUsize,
281}
282
283impl<'a> Route<'a> {
284    /// Create a new, empty route.
285    pub fn root() -> Self {
286        Self {
287            id: None,
288            outer: None,
289            len: 0,
290            upper: AtomicUsize::new(0),
291        }
292    }
293
294    /// Extend the route with another segment with a default length of 1.
295    pub fn extend(outer: Tracked<'a, Self>) -> Self {
296        Route {
297            outer: Some(outer),
298            id: None,
299            len: 1,
300            upper: AtomicUsize::new(usize::MAX),
301        }
302    }
303
304    /// Attach a file id to the route segment.
305    pub fn with_id(self, id: FileId) -> Self {
306        Self { id: Some(id), ..self }
307    }
308
309    /// Set the length of the route segment to zero.
310    pub fn unnested(self) -> Self {
311        Self { len: 0, ..self }
312    }
313
314    /// Start tracking this route.
315    ///
316    /// In comparison to [`Track::track`], this method skips this chain link
317    /// if it does not contribute anything.
318    pub fn track(&self) -> Tracked<'_, Self> {
319        match self.outer {
320            Some(outer) if self.id.is_none() && self.len == 0 => outer,
321            _ => Track::track(self),
322        }
323    }
324
325    /// Increase the nesting depth for this route segment.
326    pub fn increase(&mut self) {
327        self.len += 1;
328    }
329
330    /// Decrease the nesting depth for this route segment.
331    pub fn decrease(&mut self) {
332        self.len -= 1;
333    }
334}
335
336/// The maximum nesting depths. They are different so that even if show rule and
337/// call checks are interleaved, for show rule problems we always get the show
338/// rule error. The lower the max depth for a kind of error, the higher its
339/// precedence compared to the others.
340impl Route<'_> {
341    /// The maximum stack nesting depth.
342    const MAX_SHOW_RULE_DEPTH: usize = 64;
343
344    /// The maximum layout nesting depth.
345    const MAX_LAYOUT_DEPTH: usize = 72;
346
347    /// The maximum HTML nesting depth.
348    const MAX_HTML_DEPTH: usize = 72;
349
350    /// The maximum function call nesting depth.
351    const MAX_CALL_DEPTH: usize = 80;
352
353    /// Ensures that we are within the maximum show rule depth.
354    pub fn check_show_depth(&self) -> HintedStrResult<()> {
355        if !self.within(Route::MAX_SHOW_RULE_DEPTH) {
356            bail!(
357                "maximum show rule depth exceeded";
358                hint: "maybe a show rule matches its own output";
359                hint: "maybe there are too deeply nested elements";
360            );
361        }
362        Ok(())
363    }
364
365    /// Ensures that we are within the maximum layout depth.
366    pub fn check_layout_depth(&self) -> HintedStrResult<()> {
367        if !self.within(Route::MAX_LAYOUT_DEPTH) {
368            bail!(
369                "maximum layout depth exceeded";
370                hint: "try to reduce the amount of nesting in your layout";
371            );
372        }
373        Ok(())
374    }
375
376    /// Ensures that we are within the maximum HTML depth.
377    pub fn check_html_depth(&self) -> HintedStrResult<()> {
378        if !self.within(Route::MAX_HTML_DEPTH) {
379            bail!(
380                "maximum HTML depth exceeded";
381                hint: "try to reduce the amount of nesting of your HTML";
382            );
383        }
384        Ok(())
385    }
386
387    /// Ensures that we are within the maximum function call depth.
388    pub fn check_call_depth(&self) -> StrResult<()> {
389        if !self.within(Route::MAX_CALL_DEPTH) {
390            bail!("maximum function call depth exceeded");
391        }
392        Ok(())
393    }
394}
395
396#[comemo::track]
397#[allow(clippy::needless_lifetimes)]
398impl<'a> Route<'a> {
399    /// Whether the given id is part of the route.
400    pub fn contains(&self, id: FileId) -> bool {
401        self.id == Some(id) || self.outer.is_some_and(|outer| outer.contains(id))
402    }
403
404    /// Whether the route's depth is less than or equal to the given depth.
405    pub fn within(&self, depth: usize) -> bool {
406        // We only need atomicity and no synchronization of other operations, so
407        // `Relaxed` is fine.
408        use Ordering::Relaxed;
409
410        let upper = self.upper.load(Relaxed);
411        if upper.saturating_add(self.len) <= depth {
412            return true;
413        }
414
415        match self.outer {
416            Some(_) if depth < self.len => false,
417            Some(outer) => {
418                let within = outer.within(depth - self.len);
419                if within && depth < upper {
420                    // We don't want to accidentally increase the upper bound,
421                    // hence the compare-exchange.
422                    self.upper.compare_exchange(upper, depth, Relaxed, Relaxed).ok();
423                }
424                within
425            }
426            None => true,
427        }
428    }
429}
430
431impl Default for Route<'_> {
432    fn default() -> Self {
433        Self::root()
434    }
435}
436
437impl Clone for Route<'_> {
438    fn clone(&self) -> Self {
439        Self {
440            outer: self.outer,
441            id: self.id,
442            len: self.len,
443            upper: AtomicUsize::new(self.upper.load(Ordering::Relaxed)),
444        }
445    }
446}