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