typst_library/engine.rs
1//! Definition of the central compilation context.
2
3use std::collections::HashSet;
4use std::sync::atomic::{AtomicUsize, Ordering};
5
6use comemo::{Track, Tracked, TrackedMut, Validate};
7use ecow::EcoVec;
8use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
9use typst_syntax::{FileId, Span};
10
11use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
12use crate::foundations::{Styles, Value};
13use crate::introspection::Introspector;
14use crate::routines::Routines;
15use crate::World;
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>(&mut self, iter: P, f: F) -> impl Iterator<Item = U>
51 where
52 P: IntoIterator<IntoIter = I>,
53 I: Iterator<Item = T>,
54 T: Send,
55 U: Send,
56 F: Fn(&mut Engine, T) -> U + Send + Sync,
57 {
58 let Engine {
59 world, introspector, traced, ref route, routines, ..
60 } = *self;
61
62 // We collect into a vector and then call `into_par_iter` instead of
63 // using `par_bridge` because it does not retain the ordering.
64 let work: Vec<T> = iter.into_iter().collect();
65
66 // Work in parallel.
67 let mut pairs: Vec<(U, Sink)> = Vec::with_capacity(work.len());
68 work.into_par_iter()
69 .map(|value| {
70 let mut sink = Sink::new();
71 let mut engine = Engine {
72 world,
73 introspector,
74 traced,
75 sink: sink.track_mut(),
76 route: route.clone(),
77 routines,
78 };
79 (f(&mut engine, value), sink)
80 })
81 .collect_into_vec(&mut pairs);
82
83 // Apply the subsinks to the outer sink.
84 for (_, sink) in &mut pairs {
85 let sink = std::mem::take(sink);
86 self.sink.extend(sink.delayed, sink.warnings, sink.values);
87 }
88
89 pairs.into_iter().map(|(output, _)| output)
90 }
91}
92
93/// May hold a span that is currently under inspection.
94#[derive(Default)]
95pub struct Traced(Option<Span>);
96
97impl Traced {
98 /// Wraps a to-be-traced `Span`.
99 ///
100 /// Call `Traced::default()` to trace nothing.
101 pub fn new(traced: Span) -> Self {
102 Self(Some(traced))
103 }
104}
105
106#[comemo::track]
107impl Traced {
108 /// Returns the traced span _if_ it is part of the given source file or
109 /// `None` otherwise.
110 ///
111 /// We hide the span if it isn't in the given file so that only results for
112 /// the file with the traced span are invalidated.
113 pub fn get(&self, id: FileId) -> Option<Span> {
114 if self.0.and_then(Span::id) == Some(id) {
115 self.0
116 } else {
117 None
118 }
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: HashSet<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 Validate>::Constraint>>,
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: "check whether the show rule matches its own output"
316 );
317 }
318 Ok(())
319 }
320
321 /// Ensures that we are within the maximum layout depth.
322 pub fn check_layout_depth(&self) -> HintedStrResult<()> {
323 if !self.within(Route::MAX_LAYOUT_DEPTH) {
324 bail!(
325 "maximum layout depth exceeded";
326 hint: "try to reduce the amount of nesting in your layout",
327 );
328 }
329 Ok(())
330 }
331
332 /// Ensures that we are within the maximum HTML depth.
333 pub fn check_html_depth(&self) -> HintedStrResult<()> {
334 if !self.within(Route::MAX_HTML_DEPTH) {
335 bail!(
336 "maximum HTML depth exceeded";
337 hint: "try to reduce the amount of nesting of your HTML",
338 );
339 }
340 Ok(())
341 }
342
343 /// Ensures that we are within the maximum function call depth.
344 pub fn check_call_depth(&self) -> StrResult<()> {
345 if !self.within(Route::MAX_CALL_DEPTH) {
346 bail!("maximum function call depth exceeded");
347 }
348 Ok(())
349 }
350}
351
352#[comemo::track]
353#[allow(clippy::needless_lifetimes)]
354impl<'a> Route<'a> {
355 /// Whether the given id is part of the route.
356 pub fn contains(&self, id: FileId) -> bool {
357 self.id == Some(id) || self.outer.is_some_and(|outer| outer.contains(id))
358 }
359
360 /// Whether the route's depth is less than or equal to the given depth.
361 pub fn within(&self, depth: usize) -> bool {
362 // We only need atomicity and no synchronization of other operations, so
363 // `Relaxed` is fine.
364 use Ordering::Relaxed;
365
366 let upper = self.upper.load(Relaxed);
367 if upper.saturating_add(self.len) <= depth {
368 return true;
369 }
370
371 match self.outer {
372 Some(_) if depth < self.len => false,
373 Some(outer) => {
374 let within = outer.within(depth - self.len);
375 if within && depth < upper {
376 // We don't want to accidentally increase the upper bound,
377 // hence the compare-exchange.
378 self.upper.compare_exchange(upper, depth, Relaxed, Relaxed).ok();
379 }
380 within
381 }
382 None => true,
383 }
384 }
385}
386
387impl Default for Route<'_> {
388 fn default() -> Self {
389 Self::root()
390 }
391}
392
393impl Clone for Route<'_> {
394 fn clone(&self) -> Self {
395 Self {
396 outer: self.outer,
397 id: self.id,
398 len: self.len,
399 upper: AtomicUsize::new(self.upper.load(Ordering::Relaxed)),
400 }
401 }
402}