diff options
Diffstat (limited to 'crates/typst-library/src/meta/state.rs')
| -rw-r--r-- | crates/typst-library/src/meta/state.rs | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst-library/src/meta/state.rs new file mode 100644 index 00000000..aee53a29 --- /dev/null +++ b/crates/typst-library/src/meta/state.rs @@ -0,0 +1,440 @@ +use std::fmt::{self, Debug, Formatter, Write}; + +use ecow::{eco_vec, EcoVec}; +use typst::eval::Tracer; +use typst::model::DelayedErrors; + +use crate::prelude::*; + +/// Manages stateful parts of your document. +/// +/// Let's say you have some computations in your document and want to remember +/// the result of your last computation to use it in the next one. You might try +/// something similar to the code below and expect it to output 10, 13, 26, and +/// 21. However this **does not work** in Typst. If you test this code, you will +/// see that Typst complains with the following error message: _Variables from +/// outside the function are read-only and cannot be modified._ +/// +/// ```typ +/// #let x = 0 +/// #let compute(expr) = { +/// x = eval( +/// expr.replace("x", str(x)) +/// ) +/// [New value is #x. ] +/// } +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ``` +/// +/// ## State and document markup { #state-and-markup } +/// Why does it do that? Because, in general, this kind of computation with side +/// effects is problematic in document markup and Typst is upfront about that. +/// For the results to make sense, the computation must proceed in the same +/// order in which the results will be laid out in the document. In our simple +/// example, that's the case, but in general it might not be. +/// +/// Let's look at a slightly different, but similar kind of state: The heading +/// numbering. We want to increase the heading counter at each heading. Easy +/// enough, right? Just add one. Well, it's not that simple. Consider the +/// following example: +/// +/// ```example +/// #set heading(numbering: "1.") +/// #let template(body) = [ +/// = Outline +/// ... +/// #body +/// ] +/// +/// #show: template +/// +/// = Introduction +/// ... +/// ``` +/// +/// Here, Typst first processes the body of the document after the show rule, +/// sees the `Introduction` heading, then passes the resulting content to the +/// `template` function and only then sees the `Outline`. Just counting up would +/// number the `Introduction` with `1` and the `Outline` with `2`. +/// +/// ## Managing state in Typst { #state-in-typst } +/// So what do we do instead? We use Typst's state management system. Calling +/// the `state` function with an identifying string key and an optional initial +/// value gives you a state value which exposes a few methods. The two most +/// important ones are `display` and `update`: +/// +/// - The `display` method shows the current value of the state. You can +/// optionally give it a function that receives the value and formats it in +/// some way. +/// +/// - The `update` method modifies the state. You can give it any value. If +/// given a non-function value, it sets the state to that value. If given a +/// function, that function receives the previous state and has to return the +/// new state. +/// +/// Our initial example would now look like this: +/// +/// ```example +/// #let s = state("x", 0) +/// #let compute(expr) = [ +/// #s.update(x => +/// eval(expr.replace("x", str(x))) +/// ) +/// New value is #s.display(). +/// ] +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ``` +/// +/// State managed by Typst is always updated in layout order, not in evaluation +/// order. The `update` method returns content and its effect occurs at the +/// position where the returned content is inserted into the document. +/// +/// As a result, we can now also store some of the computations in +/// variables, but they still show the correct results: +/// +/// ```example +/// >>> #let s = state("x", 0) +/// >>> #let compute(expr) = [ +/// >>> #s.update(x => +/// >>> eval(expr.replace("x", str(x))) +/// >>> ) +/// >>> New value is #s.display(). +/// >>> ] +/// <<< ... +/// +/// #let more = [ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ] +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// #more +/// ``` +/// +/// This example is of course a bit silly, but in practice this is often exactly +/// what you want! A good example are heading counters, which is why Typst's +/// [counting system]($func/counter) is very similar to its state system. +/// +/// ## Time Travel { #time-travel } +/// By using Typst's state management system you also get time travel +/// capabilities! By combining the state system with [`locate`]($func/locate) +/// and [`query`]($func/query), we can find out what the value of the state will +/// be at any position in the document from anywhere else. In particular, the +/// `at` method gives us the value of the state at any location and the `final` +/// methods gives us the value of the state at the end of the document. +/// +/// ```example +/// >>> #let s = state("x", 0) +/// >>> #let compute(expr) = [ +/// >>> #s.update(x => { +/// >>> eval(expr.replace("x", str(x))) +/// >>> }) +/// >>> New value is #s.display(). +/// >>> ] +/// <<< ... +/// +/// Value at `<here>` is +/// #locate(loc => s.at( +/// query(<here>, loc) +/// .first() +/// .location() +/// )) +/// +/// #compute("10") \ +/// #compute("x + 3") \ +/// *Here.* <here> \ +/// #compute("x * 2") \ +/// #compute("x - 5") +/// ``` +/// +/// ## A word of caution { #caution } +/// To resolve the values of all states, Typst evaluates parts of your code +/// multiple times. However, there is no guarantee that your state manipulation +/// can actually be completely resolved. +/// +/// For instance, if you generate state updates depending on the final value of +/// a state, the results might never converge. The example below illustrates +/// this. We initialize our state with `1` and then update it to its own final +/// value plus 1. So it should be `2`, but then its final value is `2`, so it +/// should be `3`, and so on. This example display `4` because Typst simply +/// gives up after a few attempts. +/// +/// ```example +/// #let s = state("x", 1) +/// #locate(loc => { +/// s.update(s.final(loc) + 1) +/// }) +/// #s.display() +/// ``` +/// +/// In general, you should _typically_ not generate state updates from within +/// `locate` calls or `display` calls of state or counters. Instead, pass a +/// function to `update` that determines the value of the state based on its +/// previous value. +/// +/// ## Methods +/// ### display() +/// Displays the value of the state. +/// +/// - format: function (positional) +/// A function which receives the value of the state and can return arbitrary +/// content which is then displayed. If this is omitted, the value is directly +/// displayed. +/// +/// - returns: content +/// +/// ### update() +/// Updates the value of the state. +/// +/// The update will be in effect at the position where the returned content is +/// inserted into the document. If you don't put the output into the document, +/// nothing happens! This would be the case, for example, if you write +/// `{let _ = state("key").update(7)}`. State updates are always applied in +/// layout order and in that case, Typst wouldn't know when to update the state. +/// +/// - value: any or function (positional, required) +/// If given a non function-value, sets the state to that value. If given a +/// function, that function receives the previous state and has to return the +/// new state. +/// +/// - returns: content +/// +/// ### at() +/// Gets the value of the state at the given location. +/// +/// - location: location (positional, required) +/// The location at which the state's value should be retrieved. A suitable +/// location can be retrieved from [`locate`]($func/locate) or +/// [`query`]($func/query). +/// +/// - returns: any +/// +/// ### final() +/// Gets the value of the state at the end of the document. +/// +/// - location: location (positional, required) +/// Can be any location. Why is it required then? As noted before, Typst has +/// to evaluate parts of your code multiple times to determine the values of +/// all state. By only allowing this method within [`locate`]($func/locate) +/// calls, the amount of code that can depend on the method's result is +/// reduced. If you could call `final` directly at the top level of a module, +/// the evaluation of the whole module and its exports could depend on the +/// state's value. +/// +/// - returns: any +/// +/// Display: State +/// Category: meta +#[func] +pub fn state( + /// The key that identifies this state. + key: Str, + /// The initial value of the state. + #[default] + init: Value, +) -> State { + State { key, init } +} + +/// A state. +#[derive(Clone, PartialEq, Hash)] +pub struct State { + /// The key that identifies the state. + key: Str, + /// The initial value of the state. + init: Value, +} + +impl State { + /// Call a method on a state. + #[tracing::instrument(skip(vm))] + pub fn call_method( + self, + vm: &mut Vm, + method: &str, + mut args: Args, + span: Span, + ) -> SourceResult<Value> { + let value = match method { + "display" => self.display(args.eat()?).into_value(), + "at" => self.at(&mut vm.vt, args.expect("location")?)?, + "final" => self.final_(&mut vm.vt, args.expect("location")?)?, + "update" => self.update(args.expect("value or function")?).into_value(), + _ => bail!(span, "type state has no method `{}`", method), + }; + args.finish()?; + Ok(value) + } + + /// Display the current value of the state. + pub fn display(self, func: Option<Func>) -> Content { + DisplayElem::new(self, func).pack() + } + + /// Get the value of the state at the given location. + #[tracing::instrument(skip(self, vt))] + pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> { + let sequence = self.sequence(vt)?; + let offset = vt.introspector.query(&self.selector().before(location, true)).len(); + Ok(sequence[offset].clone()) + } + + /// Get the value of the state at the final location. + #[tracing::instrument(skip(self, vt))] + pub fn final_(self, vt: &mut Vt, _: Location) -> SourceResult<Value> { + let sequence = self.sequence(vt)?; + Ok(sequence.last().unwrap().clone()) + } + + /// Produce content that performs a state update. + pub fn update(self, update: StateUpdate) -> Content { + UpdateElem::new(self, update).pack() + } + + /// Produce the whole sequence of states. + /// + /// This has to happen just once for all states, cutting down the number + /// of state updates from quadratic to linear. + fn sequence(&self, vt: &mut Vt) -> SourceResult<EcoVec<Value>> { + self.sequence_impl( + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + ) + } + + /// Memoized implementation of `sequence`. + #[comemo::memoize] + fn sequence_impl( + &self, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + ) -> SourceResult<EcoVec<Value>> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let mut state = self.init.clone(); + let mut stops = eco_vec![state.clone()]; + + for elem in introspector.query(&self.selector()) { + let elem = elem.to::<UpdateElem>().unwrap(); + match elem.update() { + StateUpdate::Set(value) => state = value, + StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?, + } + stops.push(state.clone()); + } + + Ok(stops) + } + + /// The selector for this state's updates. + fn selector(&self) -> Selector { + Selector::Elem(UpdateElem::func(), Some(dict! { "state" => self.clone() })) + } +} + +impl Debug for State { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("state(")?; + self.key.fmt(f)?; + f.write_str(", ")?; + self.init.fmt(f)?; + f.write_char(')') + } +} + +cast! { + type State: "state", +} + +/// An update to perform on a state. +#[derive(Clone, PartialEq, Hash)] +pub enum StateUpdate { + /// Set the state to the specified value. + Set(Value), + /// Apply the given function to the state. + Func(Func), +} + +impl Debug for StateUpdate { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("..") + } +} + +cast! { + type StateUpdate: "state update", + v: Func => Self::Func(v), + v: Value => Self::Set(v), +} + +/// Executes a display of a state. +/// +/// Display: State +/// Category: special +#[element(Locatable, Show)] +struct DisplayElem { + /// The state. + #[required] + state: State, + + /// The function to display the state with. + #[required] + func: Option<Func>, +} + +impl Show for DisplayElem { + #[tracing::instrument(name = "DisplayElem::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let location = self.0.location().unwrap(); + let value = self.state().at(vt, location)?; + Ok(match self.func() { + Some(func) => func.call_vt(vt, [value])?.display(), + None => value.display(), + }) + })) + } +} + +/// Executes a display of a state. +/// +/// Display: State +/// Category: special +#[element(Locatable, Show)] +struct UpdateElem { + /// The state. + #[required] + state: State, + + /// The update to perform on the state. + #[required] + update: StateUpdate, +} + +impl Show for UpdateElem { + #[tracing::instrument(name = "UpdateElem::show")] + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(Content::empty()) + } +} |
