use std::any::{Any, TypeId};
use std::fmt::{Debug, Write};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use comemo::{Track, Tracked};
use ecow::{EcoString, EcoVec, eco_format};
use typst_syntax::Span;
use typst_utils::Protected;
use crate::World;
use crate::diag::{SourceDiagnostic, warning};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::introspection::Introspector;
pub const MAX_ITERS: usize = 5;
pub const ITER_NAMES: &[&str] =
&["iter (1)", "iter (2)", "iter (3)", "iter (4)", "iter (5)"];
const INSTANCES: usize = MAX_ITERS + 1;
#[typst_macros::time(name = "analyze introspections")]
pub fn analyze(
world: Tracked<dyn World + '_>,
introspectors: [&dyn Introspector; INSTANCES],
introspections: &[Introspection],
) -> EcoVec<SourceDiagnostic> {
let mut sink = Sink::new();
for introspection in introspections {
if let Some(warning) = introspection.0.diagnose(world, introspectors) {
sink.warn(warning);
}
}
let mut diags = sink.warnings();
if !diags.is_empty() {
let summary = warning!(
Span::detached(),
"document did not converge within five attempts";
hint: "see {} additional warning{} for more details",
diags.len(),
if diags.len() > 1 { "s" } else { "" };
hint: "see https://typst.app/help/convergence for help";
);
diags.insert(0, summary);
}
diags
}
pub trait Introspect: Debug + PartialEq + Hash + Send + Sync + Sized + 'static {
type Output: Hash;
fn introspect(
&self,
engine: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output;
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic;
}
#[derive(Debug, Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Introspection(Arc<dyn Bounds>);
impl Introspection {
pub fn new<I>(inner: I) -> Self
where
I: Introspect,
{
Self(Arc::new(inner))
}
}
impl PartialEq for Introspection {
fn eq(&self, other: &Self) -> bool {
self.0.dyn_eq(other)
}
}
trait Bounds: Debug + Send + Sync + Any + 'static {
fn diagnose(
&self,
world: Tracked<dyn World + '_>,
introspectors: [&dyn Introspector; INSTANCES],
) -> Option<SourceDiagnostic>;
fn dyn_eq(&self, other: &Introspection) -> bool;
fn dyn_hash(&self, state: &mut dyn Hasher);
}
impl<T> Bounds for T
where
T: Introspect,
{
fn diagnose(
&self,
world: Tracked<dyn World + '_>,
introspectors: [&dyn Introspector; INSTANCES],
) -> Option<SourceDiagnostic> {
let history = History::compute(world, introspectors, |engine, introspector| {
self.introspect(engine, introspector)
});
(!history.converged()).then(|| self.diagnose(&history))
}
fn dyn_eq(&self, other: &Introspection) -> bool {
let inner: &dyn Bounds = &*other.0;
let Some(other) = (inner as &dyn Any).downcast_ref::<Self>() else {
return false;
};
self == other
}
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
TypeId::of::<Self>().hash(&mut state);
self.hash(&mut state);
}
}
impl Hash for dyn Bounds {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.dyn_hash(state);
}
}
pub struct History<'a, T>([(&'a dyn Introspector, T); INSTANCES]);
impl<'a, T> History<'a, T> {
fn compute(
world: Tracked<dyn World + '_>,
introspectors: [&'a dyn Introspector; INSTANCES],
f: impl Fn(&mut Engine, Tracked<'a, dyn Introspector + '_>) -> T,
) -> Self {
Self(introspectors.map(|introspector| {
let tracked = introspector.track();
let traced = Traced::default();
let mut sink = Sink::new();
let mut engine = Engine {
library: world.library(),
world,
introspector: Protected::new(tracked),
traced: traced.track(),
sink: sink.track_mut(),
route: Route::default(),
};
(introspector, f(&mut engine, tracked))
}))
}
pub fn converged(&self) -> bool
where
T: Hash,
{
typst_utils::hash128(&self.0[MAX_ITERS - 1].1)
== typst_utils::hash128(&self.0[MAX_ITERS].1)
}
pub fn map<U>(self, mut f: impl FnMut(T) -> U) -> History<'a, U> {
History(self.0.map(move |(i, t)| (i, f(t))))
}
pub fn as_ref(&self) -> History<'a, &T> {
History(self.0.each_ref().map(|(i, t)| (*i, t)))
}
pub fn final_introspector(&self) -> &'a dyn Introspector {
self.0[MAX_ITERS].0
}
pub fn hint(&self, what: &str, mut f: impl FnMut(&T) -> EcoString) -> EcoString {
let mut hint = eco_format!("the following {what} were observed:");
for (i, (_, val)) in self.0.iter().enumerate() {
let attempt = match i {
0..MAX_ITERS => eco_format!("run {}", i + 1),
MAX_ITERS => eco_format!("final"),
_ => panic!(),
};
let output = f(val);
write!(hint, "\n- {attempt}: {output}").unwrap();
}
hint
}
}