use core::fmt;
use std::collections::HashSet;
use std::path::Path;
use std::sync::{Arc, OnceLock};
use ecow::{eco_vec, EcoVec};
use tinymist_std::error::prelude::Result;
use tinymist_std::{typst::TypstDocument, ImmutPath};
use tinymist_task::ExportTarget;
use tinymist_world::vfs::notify::{
FilesystemEvent, MemoryEvent, NotifyDeps, NotifyMessage, UpstreamUpdateEvent,
};
use tinymist_world::vfs::{FileId, FsProvider, RevisingVfs, WorkspaceResolver};
use tinymist_world::{
CompileSnapshot, CompilerFeat, CompilerUniverse, DiagnosticsTask, EntryReader, EntryState,
ExportSignal, FlagTask, HtmlCompilationTask, PagedCompilationTask, ProjectInsId, TaskInputs,
WorldComputeGraph, WorldDeps,
};
use tokio::sync::mpsc;
pub struct CompiledArtifact<F: CompilerFeat> {
pub graph: Arc<WorldComputeGraph<F>>,
pub diag: Arc<DiagnosticsTask>,
pub doc: Option<TypstDocument>,
pub deps: OnceLock<EcoVec<FileId>>,
}
impl<F: CompilerFeat> fmt::Display for CompiledArtifact<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let rev = self.graph.snap.world.revision();
write!(f, "CompiledArtifact({:?}, rev={rev:?})", self.graph.snap.id)
}
}
impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
type Target = Arc<WorldComputeGraph<F>>;
fn deref(&self) -> &Self::Target {
&self.graph
}
}
impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
fn clone(&self) -> Self {
Self {
graph: self.graph.clone(),
doc: self.doc.clone(),
diag: self.diag.clone(),
deps: self.deps.clone(),
}
}
}
impl<F: CompilerFeat> CompiledArtifact<F> {
pub fn id(&self) -> &ProjectInsId {
&self.graph.snap.id
}
pub fn success_doc(&self) -> Option<TypstDocument> {
self.doc
.as_ref()
.cloned()
.or_else(|| self.snap.success_doc.clone())
}
pub fn depended_files(&self) -> &EcoVec<FileId> {
self.deps.get_or_init(|| {
let mut deps = EcoVec::default();
self.graph.snap.world.iter_dependencies(&mut |f| {
deps.push(f);
});
deps
})
}
pub fn from_graph(graph: Arc<WorldComputeGraph<F>>, is_html: bool) -> CompiledArtifact<F> {
let _ = graph.provide::<FlagTask<HtmlCompilationTask>>(Ok(FlagTask::flag(is_html)));
let _ = graph.provide::<FlagTask<PagedCompilationTask>>(Ok(FlagTask::flag(!is_html)));
let doc = if is_html {
graph.shared_compile_html().expect("html").map(From::from)
} else {
graph.shared_compile().expect("paged").map(From::from)
};
CompiledArtifact {
diag: graph.shared_diagnostics().expect("diag"),
graph,
doc,
deps: OnceLock::default(),
}
}
pub fn error_cnt(&self) -> usize {
self.diag.error_cnt()
}
pub fn warning_cnt(&self) -> usize {
self.diag.warning_cnt()
}
pub fn diagnostics(&self) -> impl Iterator<Item = &typst::diag::SourceDiagnostic> {
self.diag.diagnostics()
}
pub fn has_errors(&self) -> bool {
self.error_cnt() > 0
}
pub fn with_signal(mut self, signal: ExportSignal) -> Self {
let mut snap = self.snap.clone();
snap.signal = signal;
self.graph = self.graph.snapshot_unsafe(snap);
self
}
}
#[derive(Debug, Clone)]
pub struct CompileReport {
pub id: ProjectInsId,
pub compiling_id: Option<FileId>,
pub page_count: u32,
pub status: CompileStatusEnum,
}
#[derive(Debug, Clone)]
pub enum CompileStatusEnum {
Suspend,
Compiling,
CompileSuccess(CompileStatusResult),
CompileError(CompileStatusResult),
ExportError(CompileStatusResult),
}
#[derive(Debug, Clone)]
pub struct CompileStatusResult {
diag: u32,
elapsed: tinymist_std::time::Duration,
}
#[allow(missing_docs)]
impl CompileReport {
pub fn message(&self) -> CompileReportMsg<'_> {
CompileReportMsg(self)
}
}
#[allow(missing_docs)]
pub struct CompileReportMsg<'a>(&'a CompileReport);
impl fmt::Display for CompileReportMsg<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use CompileStatusEnum::*;
use CompileStatusResult as Res;
let input = WorkspaceResolver::display(self.0.compiling_id);
let (stage, Res { diag, elapsed }) = match &self.0.status {
Suspend => return f.write_str("suspended"),
Compiling => return f.write_str("compiling"),
CompileSuccess(Res { diag: 0, elapsed }) => {
return write!(f, "{input:?}: compilation succeeded in {elapsed:?}")
}
CompileSuccess(res) => ("compilation succeeded", res),
CompileError(res) => ("compilation failed", res),
ExportError(res) => ("export failed", res),
};
write!(f, "{input:?}: {stage} with {diag} warnings in {elapsed:?}")
}
}
pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
fn notify_compile(&self, res: &CompiledArtifact<F>);
fn notify_removed(&self, _id: &ProjectInsId) {}
fn status(&self, revision: usize, rep: CompileReport);
}
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> CompileHandler<F, Ext>
for std::marker::PhantomData<fn(F, Ext)>
{
fn on_any_compile_reason(&self, _state: &mut ProjectCompiler<F, Ext>) {
log::info!("ProjectHandle: no need to compile");
}
fn notify_compile(&self, _res: &CompiledArtifact<F>) {}
fn status(&self, _revision: usize, _rep: CompileReport) {}
}
pub enum Interrupt<F: CompilerFeat> {
Compile(ProjectInsId),
Settle(ProjectInsId),
Compiled(CompiledArtifact<F>),
ChangeTask(ProjectInsId, TaskInputs),
Font(Arc<F::FontResolver>),
Memory(MemoryEvent),
Fs(FilesystemEvent),
}
impl<F: CompilerFeat> fmt::Debug for Interrupt<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Interrupt::Compile(id) => write!(f, "Compile({id:?})"),
Interrupt::Settle(id) => write!(f, "Settle({id:?})"),
Interrupt::Compiled(artifact) => write!(f, "Compiled({:?})", artifact.id()),
Interrupt::ChangeTask(id, change) => {
write!(f, "ChangeTask({id:?}, entry={:?})", change.entry.is_some())
}
Interrupt::Font(..) => write!(f, "Font(..)"),
Interrupt::Memory(..) => write!(f, "Memory(..)"),
Interrupt::Fs(..) => write!(f, "Fs(..)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CompileReasons {
pub by_memory_events: bool,
pub by_fs_events: bool,
pub by_entry_update: bool,
}
impl From<CompileReasons> for ExportSignal {
fn from(value: CompileReasons) -> Self {
Self {
by_mem_events: value.by_memory_events,
by_fs_events: value.by_fs_events,
by_entry_update: value.by_entry_update,
}
}
}
impl CompileReasons {
pub fn see(&mut self, reason: CompileReasons) {
self.by_memory_events |= reason.by_memory_events;
self.by_fs_events |= reason.by_fs_events;
self.by_entry_update |= reason.by_entry_update;
}
pub fn any(&self) -> bool {
self.by_memory_events || self.by_fs_events || self.by_entry_update
}
pub fn exclude(&self, excluded: Self) -> Self {
Self {
by_memory_events: self.by_memory_events && !excluded.by_memory_events,
by_fs_events: self.by_fs_events && !excluded.by_fs_events,
by_entry_update: self.by_entry_update && !excluded.by_entry_update,
}
}
}
fn no_reason() -> CompileReasons {
CompileReasons::default()
}
fn reason_by_mem() -> CompileReasons {
CompileReasons {
by_memory_events: true,
..CompileReasons::default()
}
}
fn reason_by_fs() -> CompileReasons {
CompileReasons {
by_fs_events: true,
..CompileReasons::default()
}
}
fn reason_by_entry_change() -> CompileReasons {
CompileReasons {
by_entry_update: true,
..CompileReasons::default()
}
}
struct TaggedMemoryEvent {
logical_tick: usize,
event: MemoryEvent,
}
pub struct CompileServerOpts<F: CompilerFeat, Ext> {
pub handler: Arc<dyn CompileHandler<F, Ext>>,
pub enable_watch: bool,
pub export_target: ExportTarget,
}
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> Default for CompileServerOpts<F, Ext> {
fn default() -> Self {
Self {
handler: Arc::new(std::marker::PhantomData),
enable_watch: false,
export_target: ExportTarget::Paged,
}
}
}
pub struct ProjectCompiler<F: CompilerFeat, Ext> {
pub handler: Arc<dyn CompileHandler<F, Ext>>,
export_target: ExportTarget,
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
pub enable_watch: bool,
logical_tick: usize,
dirty_shadow_logical_tick: usize,
estimated_shadow_files: HashSet<Arc<Path>>,
pub primary: ProjectInsState<F, Ext>,
pub dedicates: Vec<ProjectInsState<F, Ext>>,
deps: ProjectDeps,
}
impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
pub fn new(
verse: CompilerUniverse<F>,
dep_tx: mpsc::UnboundedSender<NotifyMessage>,
CompileServerOpts {
handler,
enable_watch,
export_target,
}: CompileServerOpts<F, Ext>,
) -> Self {
let primary = Self::create_project(
ProjectInsId("primary".into()),
verse,
export_target,
handler.clone(),
);
Self {
handler,
dep_tx,
enable_watch,
export_target,
logical_tick: 1,
dirty_shadow_logical_tick: 0,
estimated_shadow_files: Default::default(),
primary,
deps: Default::default(),
dedicates: vec![],
}
}
pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
self.primary.snapshot()
}
pub fn compile_once(&mut self) -> CompiledArtifact<F> {
let snap = self.primary.make_snapshot();
ProjectInsState::run_compile(self.handler.clone(), snap, self.export_target)()
}
pub fn projects(&mut self) -> impl Iterator<Item = &mut ProjectInsState<F, Ext>> {
std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut())
}
fn create_project(
id: ProjectInsId,
verse: CompilerUniverse<F>,
export_target: ExportTarget,
handler: Arc<dyn CompileHandler<F, Ext>>,
) -> ProjectInsState<F, Ext> {
ProjectInsState {
id,
ext: Default::default(),
verse,
reason: no_reason(),
snapshot: None,
handler,
export_target,
compilation: OnceLock::default(),
latest_success_doc: None,
deps: Default::default(),
committed_revision: 0,
}
}
pub fn find_project<'a>(
primary: &'a mut ProjectInsState<F, Ext>,
dedicates: &'a mut [ProjectInsState<F, Ext>],
id: &ProjectInsId,
) -> &'a mut ProjectInsState<F, Ext> {
if id == &primary.id {
return primary;
}
dedicates.iter_mut().find(|e| e.id == *id).unwrap()
}
pub fn clear_dedicates(&mut self) {
self.dedicates.clear();
}
pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> Result<ProjectInsId> {
let id = ProjectInsId(group.into());
let verse = CompilerUniverse::<F>::new_raw(
entry,
self.primary.verse.features.clone(),
Some(self.primary.verse.inputs().clone()),
self.primary.verse.vfs().fork(),
self.primary.verse.registry.clone(),
self.primary.verse.font_resolver.clone(),
);
let mut proj =
Self::create_project(id.clone(), verse, self.export_target, self.handler.clone());
proj.reason.see(reason_by_entry_change());
self.remove_dedicates(&id);
self.dedicates.push(proj);
Ok(id)
}
fn remove_dedicates(&mut self, id: &ProjectInsId) {
let proj = self.dedicates.iter().position(|e| e.id == *id);
if let Some(idx) = proj {
self.handler.notify_removed(id);
self.deps.project_deps.remove_mut(id);
let _proj = self.dedicates.remove(idx);
let res = self
.dep_tx
.send(NotifyMessage::SyncDependency(Box::new(self.deps.clone())));
log_send_error("dep_tx", res);
} else {
log::warn!("ProjectCompiler: settle project not found {id:?}");
}
}
pub fn process(&mut self, intr: Interrupt<F>) {
self.process_inner(intr);
self.handler.clone().on_any_compile_reason(self);
}
fn process_inner(&mut self, intr: Interrupt<F>) {
match intr {
Interrupt::Compile(id) => {
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
proj.verse.increment_revision(|verse| {
verse.flush();
});
proj.reason.see(reason_by_entry_change());
}
Interrupt::Compiled(artifact) => {
let proj =
Self::find_project(&mut self.primary, &mut self.dedicates, artifact.id());
let processed = proj.process_compile(artifact);
if processed {
self.deps
.project_deps
.insert_mut(proj.id.clone(), proj.deps.clone());
let event = NotifyMessage::SyncDependency(Box::new(self.deps.clone()));
let err = self.dep_tx.send(event);
log_send_error("dep_tx", err);
}
}
Interrupt::Settle(id) => {
self.remove_dedicates(&id);
}
Interrupt::ChangeTask(id, change) => {
let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
proj.verse.increment_revision(|verse| {
if let Some(inputs) = change.inputs.clone() {
verse.set_inputs(inputs);
}
if let Some(entry) = change.entry.clone() {
let res = verse.mutate_entry(entry);
if let Err(err) = res {
log::error!("ProjectCompiler: change entry error: {err:?}");
}
}
});
if let Some(entry) = change.entry {
if entry.is_inactive() {
log::info!("ProjectCompiler: removing diag");
self.handler.status(proj.verse.revision.get(), {
CompileReport {
id: proj.id.clone(),
compiling_id: None,
page_count: 0,
status: CompileStatusEnum::Suspend,
}
});
}
proj.latest_success_doc = None;
}
proj.reason.see(reason_by_entry_change());
}
Interrupt::Font(fonts) => {
self.projects().for_each(|proj| {
let font_changed = proj.verse.increment_revision(|verse| {
verse.set_fonts(fonts.clone());
verse.font_changed()
});
if font_changed {
proj.reason.see(reason_by_entry_change());
}
});
}
Interrupt::Memory(event) => {
log::debug!("ProjectCompiler: memory event incoming");
let mut files = HashSet::new();
if matches!(event, MemoryEvent::Sync(..)) {
std::mem::swap(&mut files, &mut self.estimated_shadow_files);
}
let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
for path in &e.removes {
self.estimated_shadow_files.remove(path);
files.insert(Arc::clone(path));
}
for (path, _) in &e.inserts {
self.estimated_shadow_files.insert(Arc::clone(path));
files.remove(path);
}
if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
for (proj, event) in proj.zip(changes) {
log::debug!("memory update: vfs {:#?}", proj.verse.vfs().display());
let vfs_changed = proj.verse.increment_revision(|verse| {
log::debug!("memory update: {:?}", proj.id);
Self::apply_memory_changes(&mut verse.vfs(), event.clone());
log::debug!("memory update: changed {}", verse.vfs_changed());
verse.vfs_changed()
});
if vfs_changed {
proj.reason.see(reason_by_mem());
}
log::debug!("memory update: vfs after {:#?}", proj.verse.vfs().display());
}
return;
}
self.dirty_shadow_logical_tick = self.logical_tick;
let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
invalidates: files.into_iter().collect(),
opaque: Box::new(TaggedMemoryEvent {
logical_tick: self.logical_tick,
event,
}),
});
let err = self.dep_tx.send(event);
log_send_error("dep_tx", err);
}
Interrupt::Fs(event) => {
log::debug!("ProjectCompiler: fs event incoming {event:?}");
let dirty_tick = &mut self.dirty_shadow_logical_tick;
let (changes, event) = event.split();
let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
for (proj, changes) in proj.zip(changes) {
let vfs_changed = proj.verse.increment_revision(|verse| {
{
let mut vfs = verse.vfs();
if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
.is_none()
{
log::warn!("ProjectCompiler: unknown upstream update event");
proj.reason.see(reason_by_mem());
}
vfs.notify_fs_changes(changes);
}
verse.vfs_changed()
});
if vfs_changed {
proj.reason.see(reason_by_fs());
}
}
}
}
}
fn apply_delayed_memory_changes(
verse: &mut RevisingVfs<'_, F::AccessModel>,
dirty_shadow_logical_tick: &mut usize,
event: &Option<UpstreamUpdateEvent>,
) -> Option<()> {
if let Some(event) = event {
let TaggedMemoryEvent {
logical_tick,
event,
} = event.opaque.as_ref().downcast_ref()?;
if logical_tick == dirty_shadow_logical_tick {
*dirty_shadow_logical_tick = 0;
}
Self::apply_memory_changes(verse, event.clone());
}
Some(())
}
fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
if matches!(event, MemoryEvent::Sync(..)) {
vfs.reset_shadow();
}
match event {
MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
for path in event.removes {
let _ = vfs.unmap_shadow(&path);
}
for (path, snap) in event.inserts {
let _ = vfs.map_shadow(&path, snap);
}
}
}
}
}
pub struct ProjectInsState<F: CompilerFeat, Ext> {
pub id: ProjectInsId,
pub ext: Ext,
pub verse: CompilerUniverse<F>,
pub export_target: ExportTarget,
pub reason: CompileReasons,
snapshot: Option<Arc<WorldComputeGraph<F>>>,
pub compilation: OnceLock<CompiledArtifact<F>>,
pub handler: Arc<dyn CompileHandler<F, Ext>>,
deps: EcoVec<ImmutPath>,
latest_success_doc: Option<TypstDocument>,
committed_revision: usize,
}
impl<F: CompilerFeat, Ext: 'static> ProjectInsState<F, Ext> {
pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
match self.snapshot.as_ref() {
Some(snap) if snap.world().revision() == self.verse.revision => snap.clone(),
_ => {
let snap = self.make_snapshot();
self.snapshot = Some(snap.clone());
snap
}
}
}
fn make_snapshot(&self) -> Arc<WorldComputeGraph<F>> {
let world = self.verse.snapshot();
let snap = CompileSnapshot {
id: self.id.clone(),
world,
signal: ExportSignal {
by_entry_update: self.reason.by_entry_update,
by_mem_events: self.reason.by_memory_events,
by_fs_events: self.reason.by_fs_events,
},
success_doc: self.latest_success_doc.clone(),
};
WorldComputeGraph::new(snap)
}
#[must_use]
pub fn may_compile2(
&mut self,
compute: impl FnOnce(&Arc<WorldComputeGraph<F>>),
) -> Option<impl FnOnce() -> Arc<WorldComputeGraph<F>>> {
if !self.reason.any() || self.verse.entry_state().is_inactive() {
return None;
}
let snap = self.snapshot();
self.reason = Default::default();
Some(move || {
compute(&snap);
snap
})
}
#[must_use]
pub fn may_compile(
&mut self,
handler: &Arc<dyn CompileHandler<F, Ext>>,
) -> Option<impl FnOnce() -> CompiledArtifact<F>> {
if !self.reason.any() || self.verse.entry_state().is_inactive() {
return None;
}
let snap = self.snapshot();
self.reason = Default::default();
Some(Self::run_compile(handler.clone(), snap, self.export_target))
}
fn run_compile(
h: Arc<dyn CompileHandler<F, Ext>>,
graph: Arc<WorldComputeGraph<F>>,
export_target: ExportTarget,
) -> impl FnOnce() -> CompiledArtifact<F> {
let start = tinymist_std::time::now();
let id = graph.world().main_id().unwrap();
let revision = graph.world().revision().get();
h.status(revision, {
CompileReport {
id: graph.snap.id.clone(),
compiling_id: Some(id),
page_count: 0,
status: CompileStatusEnum::Compiling,
}
});
move || {
let compiled =
CompiledArtifact::from_graph(graph, matches!(export_target, ExportTarget::Html));
let res = CompileStatusResult {
diag: (compiled.warning_cnt() + compiled.error_cnt()) as u32,
elapsed: start.elapsed().unwrap_or_default(),
};
let rep = CompileReport {
id: compiled.id().clone(),
compiling_id: Some(id),
page_count: compiled.doc.as_ref().map_or(0, |doc| doc.num_of_pages()),
status: match &compiled.doc {
Some(..) => CompileStatusEnum::CompileSuccess(res),
None => CompileStatusEnum::CompileError(res),
},
};
log_compile_report(&rep);
h.status(revision, rep);
h.notify_compile(&compiled);
compiled
}
}
fn process_compile(&mut self, artifact: CompiledArtifact<F>) -> bool {
let world = &artifact.snap.world;
let compiled_revision = world.revision().get();
if self.committed_revision >= compiled_revision {
return false;
}
let doc = artifact.doc.clone();
self.committed_revision = compiled_revision;
if doc.is_some() {
self.latest_success_doc = doc;
}
let mut deps = eco_vec![];
world.iter_dependencies(&mut |dep| {
if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
deps.push(x.into())
}
});
self.deps = deps.clone();
let mut world = world.clone();
let is_primary = self.id == ProjectInsId("primary".into());
rayon::spawn(move || {
let evict_start = std::time::Instant::now();
if is_primary {
comemo::evict(10);
world.evict_source_cache(30);
}
world.evict_vfs(60);
let elapsed = evict_start.elapsed();
log::info!("ProjectCompiler: evict cache in {elapsed:?}");
});
true
}
}
fn log_compile_report(rep: &CompileReport) {
log::info!("{}", rep.message());
}
#[inline]
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
.is_ok()
}
#[derive(Debug, Clone, Default)]
struct ProjectDeps {
project_deps: rpds::RedBlackTreeMapSync<ProjectInsId, EcoVec<ImmutPath>>,
}
impl NotifyDeps for ProjectDeps {
fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath)) {
for deps in self.project_deps.values().flat_map(|e| e.iter()) {
f(deps);
}
}
}