Skip to main content

typst_library/introspection/
state.rs

1use comemo::{Track, Tracked, TrackedMut};
2use ecow::{EcoString, EcoVec, eco_format, eco_vec};
3use typst_syntax::Span;
4use typst_utils::{LazyHash, Protected};
5
6use crate::diag::{At, SourceDiagnostic, SourceResult, bail, warning};
7use crate::engine::{Engine, Route, Sink, Traced};
8use crate::foundations::{
9    Args, Construct, Content, Context, Func, LocatableSelector, NativeElement, Repr,
10    Selector, Str, Value, cast, elem, func, scope, select_where, ty,
11};
12use crate::introspection::{History, Introspect, Introspector, Locatable, Location};
13use crate::{Library, World};
14
15/// Manages stateful parts of your document.
16///
17/// Let's say you have some computations in your document and want to remember
18/// the result of your last computation to use it in the next one. You might try
19/// something similar to the code below and expect it to output 10, 13, 26,
20/// and 21. However this *does not work* in Typst. If you test this code, you
21/// will see that Typst complains with the following error message: _Variables
22/// from outside the function are read-only and cannot be modified._
23///
24/// ```typ
25/// // This doesn't work!
26/// #let star = 0
27/// #let compute(expr) = {
28///   star = eval(
29///     expr.replace("⭐", str(star))
30///   )
31///   [New value is #star.]
32/// }
33///
34/// #compute("10") \
35/// #compute("⭐ + 3") \
36/// #compute("⭐ * 2") \
37/// #compute("⭐ - 5")
38/// ```
39///
40/// = #short-or-long[State And Markup][State and document markup] <state-and-markup>
41/// Why does it do that? Because, in general, this kind of computation with side
42/// effects is problematic in document markup and Typst is upfront about that.
43/// For the results to make sense, the computation must proceed in the same
44/// order in which the results will be laid out in the document. In our simple
45/// example, that's the case, but in general it might not be.
46///
47/// Let's look at a slightly different, but similar kind of state: The heading
48/// numbering. We want to increase the heading counter at each heading. Easy
49/// enough, right? Just add one. Well, it's not that simple. Consider the
50/// following example:
51///
52/// ```example
53/// #set heading(numbering: "1.")
54/// #let template(body) = [
55///   = Outline
56///   ...
57///   #body
58/// ]
59///
60/// #show: template
61///
62/// = Introduction
63/// ...
64/// ```
65///
66/// Here, Typst first processes the body of the document after the show rule,
67/// sees the `Introduction` heading, then passes the resulting content to the
68/// `template` function and only then sees the `Outline`. Just counting up would
69/// number the `Introduction` with `1` and the `Outline` with `2`.
70///
71/// = #short-or-long[State In Typst][Managing state in Typst] <state-in-typst>
72/// So what do we do instead? We use Typst's state management system. Calling
73/// the `state` function with an identifying string key and an optional initial
74/// value gives you a state value which exposes a few functions. The two most
75/// important ones are `get` and `update`:
76///
77/// - The @state.get[`get`] function retrieves the current value of the state.
78///   Because the value can vary over the course of the document, it is a
79///   _contextual_ function that can only be used when
80///   @reference:context[context] is available.
81///
82/// - The @state.update[`update`] function modifies the state. You can give it
83///   any value. If given a non-function value, it sets the state to that value.
84///   If given a function, that function receives the previous state and has to
85///   return the new state.
86///
87/// Our initial example would now look like this:
88///
89/// ```example
90/// #let star = state("star", 0)
91/// #let compute(expr) = {
92///   star.update(old =>
93///     eval(expr.replace("⭐", str(old)))
94///   )
95///   [New value is #context star.get().]
96/// }
97///
98/// #compute("10") \
99/// #compute("⭐ + 3") \
100/// #compute("⭐ * 2") \
101/// #compute("⭐ - 5")
102/// ```
103///
104/// State managed by Typst is always updated in layout order, not in evaluation
105/// order. The `update` method returns content and its effect occurs at the
106/// position where the returned content is inserted into the document.
107///
108/// As a result, we can now also store some of the computations in variables,
109/// but they still show the correct results:
110///
111/// ```example
112/// >>> #let star = state("star", 0)
113/// >>> #let compute(expr) = {
114/// >>>   star.update(old =>
115/// >>>     eval(expr.replace("⭐", str(old)))
116/// >>>   )
117/// >>>   [New value is #context star.get().]
118/// >>> }
119/// <<< ...
120///
121/// #let more = [
122///   #compute("⭐ * 2") \
123///   #compute("⭐ - 5")
124/// ]
125///
126/// #compute("10") \
127/// #compute("⭐ + 3") \
128/// #more
129/// ```
130///
131/// This example is of course a bit silly, but in practice this is often exactly
132/// what you want! A good example are heading counters, which is why Typst's
133/// @counter[counting system] is very similar to its state system.
134///
135/// = Time Travel <time-travel>
136/// By using Typst's state management system you also get time travel
137/// capabilities! We can find out what the value of the state will be at any
138/// position in the document from anywhere else. In particular, the `at` method
139/// gives us the value of the state at any particular location and the `final`
140/// methods gives us the value of the state at the end of the document.
141///
142/// ```example
143/// >>> #let star = state("star", 0)
144/// >>> #let compute(expr) = {
145/// >>>   star.update(old =>
146/// >>>     eval(expr.replace("⭐", str(old)))
147/// >>>   )
148/// >>>   [New value is #context star.get().]
149/// >>> }
150/// <<< ...
151///
152/// Value at `<here>` is
153/// #context star.at(<here>)
154///
155/// #compute("10") \
156/// #compute("⭐ + 3") \
157/// *Here.* <here> \
158/// #compute("⭐ * 2") \
159/// #compute("⭐ - 5")
160/// ```
161///
162/// = #short-or-long[Caution][A word of caution] <caution>
163/// To resolve the values of all states, Typst evaluates parts of your code
164/// multiple times. However, there is no guarantee that your state manipulation
165/// can actually be completely resolved.
166///
167/// For instance, if you generate state updates depending on the final value of
168/// a state, the results might never converge. The example below illustrates
169/// this. We initialize our state with `1` and then update it to its own final
170/// value plus 1. So it should be `2`, but then its final value is `2`, so it
171/// should be `3`, and so on. This example displays a finite value because Typst
172/// simply gives up after a few attempts.
173///
174/// #example(
175///   ```
176///   // This is bad!
177///   #let x = state("key", 1)
178///   #context x.update(x.final() + 1)
179///   #context x.get()
180///   ```,
181///   warnings: false,
182/// )
183///
184/// In general, you should try not to generate state updates from within context
185/// expressions. If possible, try to express your updates as non-contextual
186/// values or functions that compute the new value from the previous value.
187/// Sometimes, it cannot be helped, but in those cases it is up to you to ensure
188/// that the result converges.
189#[ty(scope)]
190#[derive(Debug, Clone, PartialEq, Hash)]
191pub struct State {
192    /// The key that identifies the state.
193    key: Str,
194    /// The initial value of the state.
195    init: Value,
196}
197
198impl State {
199    /// Create a new state identified by a key.
200    pub fn new(key: Str, init: Value) -> State {
201        Self { key, init }
202    }
203
204    /// The selector for this state's updates.
205    pub fn select(&self) -> Selector {
206        select_where!(StateUpdateElem, key => self.key.clone())
207    }
208
209    /// Selects all state updates.
210    pub fn select_any() -> Selector {
211        StateUpdateElem::ELEM.select()
212    }
213}
214
215#[scope]
216impl State {
217    /// Create a new state identified by a key.
218    #[func(constructor)]
219    pub fn construct(
220        /// The key that identifies this state.
221        ///
222        /// Any @state.update[updates] to the state will be identified with the
223        /// string key. If you construct multiple states with the same `key`,
224        /// then updating any one will affect all of them.
225        key: Str,
226        /// The initial value of the state.
227        ///
228        /// If you construct multiple states with the same `key` but different
229        /// `init` values, they will each use their own initial value but share
230        /// updates. Specifically, the value of a state at some location in the
231        /// document will be computed from that state's initial value and all
232        /// preceding updates for the state's key.
233        ///
234        /// ```example
235        /// #let banana = state("key", "🍌")
236        /// #let broccoli = state("key", "🥦")
237        ///
238        /// #banana.update(it => it + "😋")
239        ///
240        /// #context [
241        ///   - #state("key", "🍎").get()
242        ///   - #banana.get()
243        ///   - #broccoli.get()
244        /// ]
245        /// ```
246        #[default]
247        init: Value,
248    ) -> State {
249        Self::new(key, init)
250    }
251
252    /// Retrieves the value of the state at the current location.
253    ///
254    /// This is equivalent to `{state.at(here())}`.
255    #[typst_macros::time(name = "state.get", span = span)]
256    #[func(contextual)]
257    pub fn get(
258        &self,
259        engine: &mut Engine,
260        context: Tracked<Context>,
261        span: Span,
262    ) -> SourceResult<Value> {
263        let loc = context.location().at(span)?;
264        engine.introspect(StateAtIntrospection(self.clone(), loc, span))
265    }
266
267    /// Retrieves the value of the state at the given selector's unique match.
268    ///
269    /// The `selector` must match exactly one element in the document. The most
270    /// useful kinds of selectors for this are @label[labels] and
271    /// @location[locations].
272    #[typst_macros::time(name = "state.at", span = span)]
273    #[func(contextual)]
274    pub fn at(
275        &self,
276        engine: &mut Engine,
277        context: Tracked<Context>,
278        span: Span,
279        /// The place at which the state's value should be retrieved.
280        selector: LocatableSelector,
281    ) -> SourceResult<Value> {
282        let loc = selector.resolve_unique(engine, context, span)?;
283        engine.introspect(StateAtIntrospection(self.clone(), loc, span))
284    }
285
286    /// Retrieves the value of the state at the end of the document.
287    #[func(contextual)]
288    pub fn final_(
289        &self,
290        engine: &mut Engine,
291        context: Tracked<Context>,
292        span: Span,
293    ) -> SourceResult<Value> {
294        context.introspect().at(span)?;
295        engine.introspect(StateFinalIntrospection(self.clone(), span))
296    }
297
298    /// Updates the value of the state.
299    ///
300    /// Returns an invisible piece of @content[content] that must be inserted
301    /// into the document to take effect. This invisible content tells Typst
302    /// that the specified update should take place wherever the content is
303    /// inserted into the document.
304    ///
305    /// State is a part of your document and runs like a thread embedded in the
306    /// document content. The value of a state is the result of all state
307    /// updates that happened in the document up until that point.
308    ///
309    /// That's why `state.update` returns an invisible sliver of content that
310    /// you need to return and include in the document — a state update that is
311    /// not "placed" in the document does not happen, and "when" it happens is
312    /// determined by where you place it. That's also why you need
313    /// @reference:context[context] to read state: You need to use the current
314    /// document position to know where on the state's "thread" you are.
315    ///
316    /// Storing a state update in a variable (e.g.
317    /// `{let my-update = state("key").update(c => c * 2)}`) will have no effect
318    /// by itself. Only once you insert the variable `[#my-update]` somewhere
319    /// into the document content, the update will take effect — at the position
320    /// where it was inserted. You can also use `[#my-update]` multiple times at
321    /// different positions. Then, the update will take effect multiple times as
322    /// well.
323    ///
324    /// In contrast to @state.get[`get`], @state.at[`at`], and
325    /// @state.final[`final`], this function does not require
326    /// @reference:context[context]. This is because, to create the state
327    /// update, we do not need to know where in the document we are. We only
328    /// need this information to resolve the state's value.
329    #[func]
330    pub fn update(
331        self,
332        span: Span,
333        /// A value to update to or a function to update with.
334        ///
335        /// - If given a non-function value, sets the state to that value.
336        /// - If given a function, that function receives the state's previous
337        ///   value and has to return the state's new value.
338        ///
339        /// When updating the state based on its previous value, you should
340        /// prefer the function form instead of retrieving the previous value
341        /// from the @reference:context[context]. This allows the compiler to
342        /// resolve the final state efficiently, minimizing the number of
343        /// @reference:context:compiler-iterations[layout iterations] required.
344        ///
345        /// In the following example, `{fill.update(f => not f)}` will paint odd
346        /// @list.item[items in the bullet list] as expected. However, if it's
347        /// replaced with `{context fill.update(not fill.get())}`, then layout
348        /// will not converge within 5 attempts, as each update will take one
349        /// additional iteration to propagate.
350        ///
351        /// ```example
352        /// #let fill = state("fill", false)
353        ///
354        /// #show list.item: it => {
355        ///   fill.update(f => not f)
356        ///   context {
357        ///     set text(fill: fuchsia) if fill.get()
358        ///     it
359        ///   }
360        /// }
361        ///
362        /// #lorem(5).split().map(list.item).join()
363        /// ```
364        update: StateUpdate,
365    ) -> Content {
366        StateUpdateElem::new(self.key, update).pack().spanned(span)
367    }
368}
369
370impl Repr for State {
371    fn repr(&self) -> EcoString {
372        eco_format!("state({}, {})", self.key.repr(), self.init.repr())
373    }
374}
375
376/// An update to perform on a state.
377#[derive(Debug, Clone, PartialEq, Hash)]
378pub enum StateUpdate {
379    /// Set the state to the specified value.
380    Set(Value),
381    /// Apply the given function to the state.
382    Func(Func),
383}
384
385cast! {
386    StateUpdate,
387    v: Func => Self::Func(v),
388    v: Value => Self::Set(v),
389}
390
391/// Executes an update of a state.
392#[elem(Construct, Locatable)]
393pub struct StateUpdateElem {
394    /// The key that identifies the state.
395    #[required]
396    key: Str,
397
398    /// The update to perform on the state.
399    #[required]
400    #[internal]
401    update: StateUpdate,
402}
403
404impl Construct for StateUpdateElem {
405    fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
406        bail!(args.span, "cannot be constructed manually");
407    }
408}
409
410/// Retrieves a state at a specific location.
411#[derive(Debug, Clone, PartialEq, Hash)]
412struct StateAtIntrospection(State, Location, Span);
413
414impl Introspect for StateAtIntrospection {
415    type Output = SourceResult<Value>;
416
417    fn introspect(
418        &self,
419        engine: &mut Engine,
420        introspector: Tracked<dyn Introspector + '_>,
421    ) -> Self::Output {
422        let Self(state, loc, _) = self;
423        let sequence = sequence(state, engine, introspector)?;
424        let offset = introspector.query_count_before(&state.select(), *loc);
425        Ok(sequence[offset].clone())
426    }
427
428    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
429        format_convergence_warning(&self.0, self.2, history)
430    }
431}
432
433/// Retrieves the final value of a state.
434#[derive(Debug, Clone, PartialEq, Hash)]
435struct StateFinalIntrospection(State, Span);
436
437impl Introspect for StateFinalIntrospection {
438    type Output = SourceResult<Value>;
439
440    fn introspect(
441        &self,
442        engine: &mut Engine,
443        introspector: Tracked<dyn Introspector + '_>,
444    ) -> Self::Output {
445        let sequence = sequence(&self.0, engine, introspector)?;
446        Ok(sequence.last().unwrap().clone())
447    }
448
449    fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
450        format_convergence_warning(&self.0, self.1, history)
451    }
452}
453
454/// Produces the whole sequence of a state.
455///
456/// Due to memoization, this has to happen just once for all retrievals of the
457/// same state, cutting down the number of computations from quadratic to
458/// linear.
459fn sequence(
460    state: &State,
461    engine: &mut Engine,
462    introspector: Tracked<dyn Introspector + '_>,
463) -> SourceResult<EcoVec<Value>> {
464    sequence_impl(
465        state,
466        engine.world,
467        engine.library,
468        introspector,
469        engine.traced,
470        TrackedMut::reborrow_mut(&mut engine.sink),
471        engine.route.track(),
472    )
473}
474
475/// Memoized implementation of `sequence`.
476#[comemo::memoize]
477fn sequence_impl(
478    state: &State,
479    world: Tracked<dyn World + '_>,
480    library: &LazyHash<Library>,
481    introspector: Tracked<dyn Introspector + '_>,
482    traced: Tracked<Traced>,
483    sink: TrackedMut<Sink>,
484    route: Tracked<Route>,
485) -> SourceResult<EcoVec<Value>> {
486    let mut engine = Engine {
487        library,
488        world,
489        introspector: Protected::from_raw(introspector),
490        traced,
491        sink,
492        route: Route::extend(route).unnested(),
493    };
494
495    let mut current = state.init.clone();
496    let mut stops = eco_vec![current.clone()];
497
498    for elem in introspector.query(&state.select()) {
499        let elem = elem.to_packed::<StateUpdateElem>().unwrap();
500        match &elem.update {
501            StateUpdate::Set(value) => current = value.clone(),
502            StateUpdate::Func(func) => {
503                current = func.call(&mut engine, Context::none().track(), [current])?
504            }
505        }
506        stops.push(current.clone());
507    }
508
509    Ok(stops)
510}
511
512/// The warning when a state failed to converge.
513fn format_convergence_warning(
514    state: &State,
515    span: Span,
516    history: &History<SourceResult<Value>>,
517) -> SourceDiagnostic {
518    warning!(span, "value of `state({})` did not converge", state.key.repr())
519        .with_hint(history.hint("values", |ret| match ret {
520            Ok(v) => eco_format!("`{}`", v.repr()),
521            Err(_) => "(errored)".into(),
522        }))
523        .with_hint("see https://typst.app/help/state-convergence for help")
524}