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}