use comemo::{Track, Tracked, TrackedMut};
use ecow::{EcoString, EcoVec, eco_format, eco_vec};
use typst_syntax::Span;
use typst_utils::{LazyHash, Protected};
use crate::diag::{At, SourceDiagnostic, SourceResult, bail, warning};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
Args, Construct, Content, Context, Func, LocatableSelector, NativeElement, Repr,
Selector, Str, Value, cast, elem, func, scope, select_where, ty,
};
use crate::introspection::{History, Introspect, Introspector, Locatable, Location};
use crate::{Library, World};
#[ty(scope)]
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct State {
key: Str,
init: Value,
}
impl State {
pub fn new(key: Str, init: Value) -> State {
Self { key, init }
}
pub fn select(&self) -> Selector {
select_where!(StateUpdateElem, key => self.key.clone())
}
pub fn select_any() -> Selector {
StateUpdateElem::ELEM.select()
}
}
#[scope]
impl State {
#[func(constructor)]
pub fn construct(
key: Str,
#[default]
init: Value,
) -> State {
Self::new(key, init)
}
#[typst_macros::time(name = "state.get", span = span)]
#[func(contextual)]
pub fn get(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Value> {
let loc = context.location().at(span)?;
engine.introspect(StateAtIntrospection(self.clone(), loc, span))
}
#[typst_macros::time(name = "state.at", span = span)]
#[func(contextual)]
pub fn at(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
selector: LocatableSelector,
) -> SourceResult<Value> {
let loc = selector.resolve_unique(engine, context, span)?;
engine.introspect(StateAtIntrospection(self.clone(), loc, span))
}
#[func(contextual)]
pub fn final_(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Value> {
context.introspect().at(span)?;
engine.introspect(StateFinalIntrospection(self.clone(), span))
}
#[func]
pub fn update(
self,
span: Span,
update: StateUpdate,
) -> Content {
StateUpdateElem::new(self.key, update).pack().spanned(span)
}
}
impl Repr for State {
fn repr(&self) -> EcoString {
eco_format!("state({}, {})", self.key.repr(), self.init.repr())
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum StateUpdate {
Set(Value),
Func(Func),
}
cast! {
StateUpdate,
v: Func => Self::Func(v),
v: Value => Self::Set(v),
}
#[elem(Construct, Locatable)]
pub struct StateUpdateElem {
#[required]
key: Str,
#[required]
#[internal]
update: StateUpdate,
}
impl Construct for StateUpdateElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
struct StateAtIntrospection(State, Location, Span);
impl Introspect for StateAtIntrospection {
type Output = SourceResult<Value>;
fn introspect(
&self,
engine: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
let Self(state, loc, _) = self;
let sequence = sequence(state, engine, introspector)?;
let offset = introspector.query_count_before(&state.select(), *loc);
Ok(sequence[offset].clone())
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(&self.0, self.2, history)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
struct StateFinalIntrospection(State, Span);
impl Introspect for StateFinalIntrospection {
type Output = SourceResult<Value>;
fn introspect(
&self,
engine: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
let sequence = sequence(&self.0, engine, introspector)?;
Ok(sequence.last().unwrap().clone())
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(&self.0, self.1, history)
}
}
fn sequence(
state: &State,
engine: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> SourceResult<EcoVec<Value>> {
sequence_impl(
state,
engine.world,
engine.library,
introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
)
}
#[comemo::memoize]
fn sequence_impl(
state: &State,
world: Tracked<dyn World + '_>,
library: &LazyHash<Library>,
introspector: Tracked<dyn Introspector + '_>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
) -> SourceResult<EcoVec<Value>> {
let mut engine = Engine {
library,
world,
introspector: Protected::from_raw(introspector),
traced,
sink,
route: Route::extend(route).unnested(),
};
let mut current = state.init.clone();
let mut stops = eco_vec![current.clone()];
for elem in introspector.query(&state.select()) {
let elem = elem.to_packed::<StateUpdateElem>().unwrap();
match &elem.update {
StateUpdate::Set(value) => current = value.clone(),
StateUpdate::Func(func) => {
current = func.call(&mut engine, Context::none().track(), [current])?
}
}
stops.push(current.clone());
}
Ok(stops)
}
fn format_convergence_warning(
state: &State,
span: Span,
history: &History<SourceResult<Value>>,
) -> SourceDiagnostic {
warning!(span, "value of `state({})` did not converge", state.key.repr())
.with_hint(history.hint("values", |ret| match ret {
Ok(v) => eco_format!("`{}`", v.repr()),
Err(_) => "(errored)".into(),
}))
.with_hint("see https://typst.app/help/state-convergence for help")
}