tinymist_project/
compiler.rs

1//! Project compiler for tinymist.
2
3use core::fmt;
4use std::collections::HashSet;
5use std::path::Path;
6use std::sync::{Arc, OnceLock};
7
8use ecow::{EcoString, EcoVec, eco_vec};
9use tinymist_std::error::prelude::Result;
10use tinymist_std::{ImmutPath, typst::TypstDocument};
11use tinymist_task::ExportTarget;
12use tinymist_world::vfs::notify::{
13    FilesystemEvent, MemoryEvent, NotifyDeps, NotifyMessage, UpstreamUpdateEvent,
14};
15use tinymist_world::vfs::{FileId, FsProvider, RevisingVfs, WorkspaceResolver};
16use tinymist_world::{
17    CompileSignal, CompileSnapshot, CompilerFeat, CompilerUniverse, DiagnosticsTask, EntryReader,
18    EntryState, FlagTask, HtmlCompilationTask, PagedCompilationTask, ProjectInsId, TaskInputs,
19    WorldComputeGraph, WorldDeps,
20};
21use tokio::sync::mpsc;
22use typst::diag::FileError;
23
24/// A compiled artifact.
25pub struct CompiledArtifact<F: CompilerFeat> {
26    /// The used compute graph.
27    pub graph: Arc<WorldComputeGraph<F>>,
28    /// The diagnostics of the document.
29    pub diag: Arc<DiagnosticsTask>,
30    /// The compiled document.
31    pub doc: Option<TypstDocument>,
32    /// The depended files.
33    pub deps: OnceLock<EcoVec<FileId>>,
34}
35
36impl<F: CompilerFeat> fmt::Display for CompiledArtifact<F> {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        let rev = self.graph.snap.world.revision();
39        write!(f, "CompiledArtifact({:?}, rev={rev:?})", self.graph.snap.id)
40    }
41}
42
43impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
44    type Target = Arc<WorldComputeGraph<F>>;
45
46    fn deref(&self) -> &Self::Target {
47        &self.graph
48    }
49}
50
51impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
52    fn clone(&self) -> Self {
53        Self {
54            graph: self.graph.clone(),
55            doc: self.doc.clone(),
56            diag: self.diag.clone(),
57            deps: self.deps.clone(),
58        }
59    }
60}
61
62impl<F: CompilerFeat> CompiledArtifact<F> {
63    /// Returns the project id.
64    pub fn id(&self) -> &ProjectInsId {
65        &self.graph.snap.id
66    }
67
68    /// Returns the last successfully compiled document.
69    pub fn success_doc(&self) -> Option<TypstDocument> {
70        self.doc
71            .as_ref()
72            .cloned()
73            .or_else(|| self.snap.success_doc.clone())
74    }
75
76    /// Returns the depended files.
77    pub fn depended_files(&self) -> &EcoVec<FileId> {
78        self.deps.get_or_init(|| {
79            let mut deps = EcoVec::default();
80            self.graph.snap.world.iter_dependencies(&mut |f| {
81                deps.push(f);
82            });
83
84            deps
85        })
86    }
87
88    /// Runs the compiler and returns the compiled document.
89    pub fn from_graph(graph: Arc<WorldComputeGraph<F>>, is_html: bool) -> CompiledArtifact<F> {
90        let _ = graph.provide::<FlagTask<HtmlCompilationTask>>(Ok(FlagTask::flag(is_html)));
91        let _ = graph.provide::<FlagTask<PagedCompilationTask>>(Ok(FlagTask::flag(!is_html)));
92        let doc = if is_html {
93            graph.shared_compile_html().expect("html").map(From::from)
94        } else {
95            graph.shared_compile().expect("paged").map(From::from)
96        };
97
98        CompiledArtifact {
99            diag: graph.shared_diagnostics().expect("diag"),
100            graph,
101            doc,
102            deps: OnceLock::default(),
103        }
104    }
105
106    /// Returns the error count.
107    pub fn error_cnt(&self) -> usize {
108        self.diag.error_cnt()
109    }
110
111    /// Returns the warning count.
112    pub fn warning_cnt(&self) -> usize {
113        self.diag.warning_cnt()
114    }
115
116    /// Returns the diagnostics.
117    pub fn diagnostics(&self) -> impl Iterator<Item = &typst::diag::SourceDiagnostic> {
118        self.diag.diagnostics()
119    }
120
121    /// Returns whether there are any errors.
122    pub fn has_errors(&self) -> bool {
123        self.error_cnt() > 0
124    }
125
126    /// Sets the signal.
127    pub fn with_signal(mut self, signal: CompileSignal) -> Self {
128        let mut snap = self.snap.clone();
129        snap.signal = signal;
130
131        self.graph = self.graph.snapshot_unsafe(snap);
132        self
133    }
134}
135
136/// The compilation status of a project.
137#[derive(Debug, Clone)]
138pub struct CompileReport {
139    /// The project ID.
140    pub id: ProjectInsId,
141    /// The file getting compiled.
142    pub compiling_id: Option<FileId>,
143    /// The number of pages in the compiled document, zero if failed.
144    pub page_count: u32,
145    /// The status of the compilation.
146    pub status: CompileStatusEnum,
147}
148
149/// The compilation status of a project.
150#[derive(Debug, Clone)]
151pub enum CompileStatusEnum {
152    /// The project is suspended.
153    Suspend,
154    /// The project is compiling.
155    Compiling,
156    /// The project compiled successfully.
157    CompileSuccess(CompileStatusResult),
158    /// The project failed to compile.
159    CompileError(CompileStatusResult),
160    /// The project failed to export.
161    ExportError(CompileStatusResult),
162}
163
164/// The compilation status result of a project.
165#[derive(Debug, Clone)]
166pub struct CompileStatusResult {
167    /// The number of errors or warnings occur.
168    diag: u32,
169    /// Used time
170    elapsed: tinymist_std::time::Duration,
171}
172
173impl CompileReport {
174    /// Gets the status message.
175    pub fn message(&self) -> CompileReportMsg<'_> {
176        CompileReportMsg(self)
177    }
178}
179
180/// A message of the compilation status.
181pub struct CompileReportMsg<'a>(&'a CompileReport);
182
183impl fmt::Display for CompileReportMsg<'_> {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        use CompileStatusEnum::*;
186        use CompileStatusResult as Res;
187
188        let input = WorkspaceResolver::display(self.0.compiling_id);
189        let (stage, Res { diag, elapsed }) = match &self.0.status {
190            Suspend => return f.write_str("suspended"),
191            Compiling => return f.write_str("compiling"),
192            CompileSuccess(Res { diag: 0, elapsed }) => {
193                return write!(f, "{input:?}: compilation succeeded in {elapsed:?}");
194            }
195            CompileSuccess(res) => ("compilation succeeded", res),
196            CompileError(res) => ("compilation failed", res),
197            ExportError(res) => ("export failed", res),
198        };
199        write!(f, "{input:?}: {stage} with {diag} warnings in {elapsed:?}")
200    }
201}
202
203/// A project compiler handler.
204pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
205    /// Called when there is any reason to compile. This doesn't mean that the
206    /// project should be compiled.
207    fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
208    // todo: notify project specific compile
209    /// Called when a compilation is done.
210    fn notify_compile(&self, res: &CompiledArtifact<F>);
211    /// Called when a project is removed.
212    fn notify_removed(&self, _id: &ProjectInsId) {}
213    /// Called when the compilation status is changed.
214    fn status(&self, revision: usize, rep: CompileReport);
215}
216
217/// No need so no compilation.
218impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> CompileHandler<F, Ext>
219    for std::marker::PhantomData<fn(F, Ext)>
220{
221    fn on_any_compile_reason(&self, _state: &mut ProjectCompiler<F, Ext>) {
222        log::info!("ProjectHandle: no need to compile");
223    }
224    fn notify_compile(&self, _res: &CompiledArtifact<F>) {}
225    fn status(&self, _revision: usize, _rep: CompileReport) {}
226}
227
228/// An interrupt to the compiler.
229pub enum Interrupt<F: CompilerFeat> {
230    /// Compile anyway.
231    Compile(ProjectInsId),
232    /// Settle a dedicated project.
233    Settle(ProjectInsId),
234    /// Compiled from computing thread.
235    Compiled(CompiledArtifact<F>),
236    /// Change the watching entry.
237    ChangeTask(ProjectInsId, TaskInputs),
238    /// Font changes.
239    Font(Arc<F::FontResolver>),
240    /// Creation timestamp changes.
241    CreationTimestamp(Option<i64>),
242    /// Memory file changes.
243    Memory(MemoryEvent),
244    /// File system event.
245    Fs(FilesystemEvent),
246    /// Save a file.
247    Save(ImmutPath),
248}
249
250impl<F: CompilerFeat> fmt::Debug for Interrupt<F> {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            Interrupt::Compile(id) => write!(f, "Compile({id:?})"),
254            Interrupt::Settle(id) => write!(f, "Settle({id:?})"),
255            Interrupt::Compiled(artifact) => write!(f, "Compiled({:?})", artifact.id()),
256            Interrupt::ChangeTask(id, change) => {
257                write!(f, "ChangeTask({id:?}, entry={:?})", change.entry.is_some())
258            }
259            Interrupt::Font(..) => write!(f, "Font(..)"),
260            Interrupt::CreationTimestamp(ts) => write!(f, "CreationTimestamp({ts:?})"),
261            Interrupt::Memory(..) => write!(f, "Memory(..)"),
262            Interrupt::Fs(..) => write!(f, "Fs(..)"),
263            Interrupt::Save(path) => write!(f, "Save({path:?})"),
264        }
265    }
266}
267
268fn no_reason() -> CompileSignal {
269    CompileSignal::default()
270}
271
272fn reason_by_mem() -> CompileSignal {
273    CompileSignal {
274        by_mem_events: true,
275        ..CompileSignal::default()
276    }
277}
278
279fn reason_by_fs() -> CompileSignal {
280    CompileSignal {
281        by_fs_events: true,
282        ..CompileSignal::default()
283    }
284}
285
286fn reason_by_entry_change() -> CompileSignal {
287    CompileSignal {
288        by_entry_update: true,
289        ..CompileSignal::default()
290    }
291}
292
293/// A tagged memory event with logical tick.
294struct TaggedMemoryEvent {
295    /// The logical tick when the event is received.
296    logical_tick: usize,
297    /// The memory event happened.
298    event: MemoryEvent,
299}
300
301/// The compiler server options.
302pub struct CompileServerOpts<F: CompilerFeat, Ext> {
303    /// The compilation handler.
304    pub handler: Arc<dyn CompileHandler<F, Ext>>,
305    /// Whether to ignoring the first fs sync event.
306    pub ignore_first_sync: bool,
307    /// Specifies the current export target.
308    pub export_target: ExportTarget,
309}
310
311impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> Default for CompileServerOpts<F, Ext> {
312    fn default() -> Self {
313        Self {
314            handler: Arc::new(std::marker::PhantomData),
315            ignore_first_sync: false,
316            export_target: ExportTarget::Paged,
317        }
318    }
319}
320
321const FILE_MISSING_ERROR_MSG: EcoString = EcoString::inline("t-file-missing");
322/// The file missing error constant.
323pub const FILE_MISSING_ERROR: FileError = FileError::Other(Some(FILE_MISSING_ERROR_MSG));
324
325/// The synchronous compiler that runs on one project or multiple projects.
326pub struct ProjectCompiler<F: CompilerFeat, Ext> {
327    /// The compilation handle.
328    pub handler: Arc<dyn CompileHandler<F, Ext>>,
329    /// Specifies the current export target.
330    export_target: ExportTarget,
331    /// Channel for sending interrupts to the compiler actor.
332    dep_tx: mpsc::UnboundedSender<NotifyMessage>,
333    /// Whether to ignore the first sync event.
334    pub ignore_first_sync: bool,
335
336    /// The current logical tick.
337    logical_tick: usize,
338    /// Last logical tick when invalidation is caused by shadow update.
339    dirty_shadow_logical_tick: usize,
340    /// Estimated latest set of shadow files.
341    estimated_shadow_files: HashSet<Arc<Path>>,
342
343    /// The primary state.
344    pub primary: ProjectInsState<F, Ext>,
345    /// The states for dedicate tasks
346    pub dedicates: Vec<ProjectInsState<F, Ext>>,
347    /// The project file dependencies.
348    deps: ProjectDeps,
349}
350
351impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
352    /// Creates a compiler with options
353    pub fn new(
354        verse: CompilerUniverse<F>,
355        dep_tx: mpsc::UnboundedSender<NotifyMessage>,
356        CompileServerOpts {
357            handler,
358            ignore_first_sync,
359            export_target,
360        }: CompileServerOpts<F, Ext>,
361    ) -> Self {
362        let primary = Self::create_project(
363            ProjectInsId("primary".into()),
364            verse,
365            export_target,
366            handler.clone(),
367        );
368        Self {
369            handler,
370            dep_tx,
371            export_target,
372
373            logical_tick: 1,
374            dirty_shadow_logical_tick: 0,
375
376            estimated_shadow_files: Default::default(),
377            ignore_first_sync,
378
379            primary,
380            deps: Default::default(),
381            dedicates: vec![],
382        }
383    }
384
385    /// Creates a snapshot of the primary project.
386    pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
387        self.primary.snapshot()
388    }
389
390    /// Compiles the document once.
391    pub fn compile_once(&mut self) -> CompiledArtifact<F> {
392        let snap = self.primary.make_snapshot();
393        ProjectInsState::run_compile(self.handler.clone(), snap, self.export_target)()
394    }
395
396    /// Gets the iterator of all projects.
397    pub fn projects(&mut self) -> impl Iterator<Item = &mut ProjectInsState<F, Ext>> {
398        std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut())
399    }
400
401    fn create_project(
402        id: ProjectInsId,
403        verse: CompilerUniverse<F>,
404        export_target: ExportTarget,
405        handler: Arc<dyn CompileHandler<F, Ext>>,
406    ) -> ProjectInsState<F, Ext> {
407        ProjectInsState {
408            id,
409            ext: Default::default(),
410            verse,
411            reason: no_reason(),
412            cached_snapshot: None,
413            handler,
414            export_target,
415            latest_compilation: OnceLock::default(),
416            latest_success_doc: None,
417            deps: Default::default(),
418            committed_revision: 0,
419        }
420    }
421
422    /// Find a project by id, but with less borrow checker restriction.
423    pub fn find_project<'a>(
424        primary: &'a mut ProjectInsState<F, Ext>,
425        dedicates: &'a mut [ProjectInsState<F, Ext>],
426        id: &ProjectInsId,
427    ) -> &'a mut ProjectInsState<F, Ext> {
428        if id == &primary.id {
429            return primary;
430        }
431
432        dedicates.iter_mut().find(|e| e.id == *id).unwrap()
433    }
434
435    /// Clear all dedicate projects.
436    pub fn clear_dedicates(&mut self) {
437        self.dedicates.clear();
438    }
439
440    /// Restart a dedicate project.
441    pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> Result<ProjectInsId> {
442        let id = ProjectInsId(group.into());
443
444        let verse = CompilerUniverse::<F>::new_raw(
445            entry,
446            self.primary.verse.features.clone(),
447            Some(self.primary.verse.inputs().clone()),
448            self.primary.verse.vfs().fork(),
449            self.primary.verse.registry.clone(),
450            self.primary.verse.font_resolver.clone(),
451            self.primary.verse.creation_timestamp,
452        );
453
454        let mut proj =
455            Self::create_project(id.clone(), verse, self.export_target, self.handler.clone());
456        proj.reason.merge(reason_by_entry_change());
457
458        self.remove_dedicates(&id);
459        self.dedicates.push(proj);
460
461        Ok(id)
462    }
463
464    fn remove_dedicates(&mut self, id: &ProjectInsId) {
465        let proj = self.dedicates.iter().position(|e| e.id == *id);
466        if let Some(idx) = proj {
467            // Resets the handle state, e.g. notified revision
468            self.handler.notify_removed(id);
469            self.deps.project_deps.remove_mut(id);
470
471            let _proj = self.dedicates.remove(idx);
472            // todo: kill compilations
473
474            let res = self
475                .dep_tx
476                .send(NotifyMessage::SyncDependency(Box::new(self.deps.clone())));
477            log_send_error("dep_tx", res);
478        } else {
479            log::warn!("ProjectCompiler: settle project not found {id:?}");
480        }
481    }
482
483    /// Process an interrupt.
484    pub fn process(&mut self, intr: Interrupt<F>) {
485        // todo: evcit cache
486        self.process_inner(intr);
487        // Customized Project Compilation Handler
488        self.handler.clone().on_any_compile_reason(self);
489    }
490
491    fn process_inner(&mut self, intr: Interrupt<F>) {
492        match intr {
493            Interrupt::Compile(id) => {
494                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
495                // Increment the revision anyway.
496                proj.verse.increment_revision(|verse| {
497                    verse.flush();
498                });
499
500                proj.reason.merge(reason_by_entry_change());
501            }
502            Interrupt::Compiled(artifact) => {
503                let proj =
504                    Self::find_project(&mut self.primary, &mut self.dedicates, artifact.id());
505
506                let processed = proj.process_compile(artifact);
507
508                if processed {
509                    self.deps
510                        .project_deps
511                        .insert_mut(proj.id.clone(), proj.deps.clone());
512
513                    let event = NotifyMessage::SyncDependency(Box::new(self.deps.clone()));
514                    let err = self.dep_tx.send(event);
515                    log_send_error("dep_tx", err);
516                }
517            }
518            Interrupt::Settle(id) => {
519                self.remove_dedicates(&id);
520            }
521            Interrupt::ChangeTask(id, change) => {
522                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
523                proj.verse.increment_revision(|verse| {
524                    if let Some(inputs) = change.inputs.clone() {
525                        verse.set_inputs(inputs);
526                    }
527
528                    if let Some(entry) = change.entry.clone() {
529                        let res = verse.mutate_entry(entry);
530                        if let Err(err) = res {
531                            log::error!("ProjectCompiler: change entry error: {err:?}");
532                        }
533                    }
534                });
535
536                // After incrementing the revision
537                if let Some(entry) = change.entry {
538                    // todo: dedicate suspended
539                    if entry.is_inactive() {
540                        log::info!("ProjectCompiler: removing diag");
541                        self.handler.status(proj.verse.revision.get(), {
542                            CompileReport {
543                                id: proj.id.clone(),
544                                compiling_id: None,
545                                page_count: 0,
546                                status: CompileStatusEnum::Suspend,
547                            }
548                        });
549                    }
550
551                    // Forget the document state of previous entry.
552                    proj.latest_success_doc = None;
553                }
554
555                proj.reason.merge(reason_by_entry_change());
556            }
557
558            Interrupt::Font(fonts) => {
559                self.projects().for_each(|proj| {
560                    let font_changed = proj.verse.increment_revision(|verse| {
561                        verse.set_fonts(fonts.clone());
562                        verse.font_changed()
563                    });
564                    if font_changed {
565                        // todo: reason_by_font_change
566                        proj.reason.merge(reason_by_entry_change());
567                    }
568                });
569            }
570            Interrupt::CreationTimestamp(creation_timestamp) => {
571                self.projects().for_each(|proj| {
572                    let timestamp_changed = proj.verse.increment_revision(|verse| {
573                        verse.set_creation_timestamp(creation_timestamp);
574                        // Creation timestamp changes affect compilation
575                        verse.creation_timestamp_changed()
576                    });
577                    if timestamp_changed {
578                        proj.reason.merge(reason_by_entry_change());
579                    }
580                });
581            }
582            Interrupt::Memory(event) => {
583                log::debug!("ProjectCompiler: memory event incoming");
584
585                // Emulate memory changes.
586                let mut files = HashSet::new();
587                if matches!(event, MemoryEvent::Sync(..)) {
588                    std::mem::swap(&mut files, &mut self.estimated_shadow_files);
589                }
590
591                let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
592                for path in &e.removes {
593                    self.estimated_shadow_files.remove(path);
594                    files.insert(Arc::clone(path));
595                }
596                for (path, _) in &e.inserts {
597                    self.estimated_shadow_files.insert(Arc::clone(path));
598                    files.remove(path);
599                }
600
601                // If there is no invalidation happening, apply memory changes directly.
602                if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
603                    let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
604                    let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
605                    for (proj, event) in proj.zip(changes) {
606                        log::debug!("memory update: vfs {:#?}", proj.verse.vfs().display());
607                        let vfs_changed = proj.verse.increment_revision(|verse| {
608                            log::debug!("memory update: {:?}", proj.id);
609                            Self::apply_memory_changes(&mut verse.vfs(), event.clone());
610                            log::debug!("memory update: changed {}", verse.vfs_changed());
611                            verse.vfs_changed()
612                        });
613                        if vfs_changed {
614                            proj.reason.merge(reason_by_mem());
615                        }
616                        log::debug!("memory update: vfs after {:#?}", proj.verse.vfs().display());
617                    }
618                    return;
619                }
620
621                // Otherwise, send upstream update event.
622                // Also, record the logical tick when shadow is dirty.
623                self.dirty_shadow_logical_tick = self.logical_tick;
624                let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
625                    invalidates: files.into_iter().collect(),
626                    opaque: Box::new(TaggedMemoryEvent {
627                        logical_tick: self.logical_tick,
628                        event,
629                    }),
630                });
631                let err = self.dep_tx.send(event);
632                log_send_error("dep_tx", err);
633            }
634            Interrupt::Save(event) => {
635                let changes = std::iter::repeat_n(&event, 1 + self.dedicates.len());
636                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
637
638                for (proj, saved_path) in proj.zip(changes) {
639                    log::debug!(
640                        "ProjectCompiler({}, rev={}): save changes",
641                        proj.verse.revision.get(),
642                        proj.id
643                    );
644
645                    // todo: only emit if saved_path is related
646                    let _ = saved_path;
647
648                    proj.reason.merge(reason_by_fs());
649                }
650            }
651            Interrupt::Fs(event) => {
652                log::debug!("ProjectCompiler: fs event incoming {event:?}");
653
654                // Apply file system changes.
655                let dirty_tick = &mut self.dirty_shadow_logical_tick;
656                let (changes, is_sync, event) = event.split_with_is_sync();
657                let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
658                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
659
660                for (proj, changes) in proj.zip(changes) {
661                    log::debug!(
662                        "ProjectCompiler({}, rev={}): fs changes applying",
663                        proj.verse.revision.get(),
664                        proj.id
665                    );
666
667                    proj.verse.increment_revision(|verse| {
668                        let mut vfs = verse.vfs();
669
670                        // Handle delayed upstream update event before applying file system
671                        // changes
672                        if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
673                            .is_none()
674                        {
675                            log::warn!("ProjectCompiler: unknown upstream update event");
676
677                            // Actual a delayed memory event.
678                            proj.reason.merge(reason_by_mem());
679                        }
680                        vfs.notify_fs_changes(changes);
681                    });
682
683                    log::debug!(
684                        "ProjectCompiler({},rev={}): fs changes applied, {is_sync}",
685                        proj.id,
686                        proj.verse.revision.get(),
687                    );
688
689                    if !self.ignore_first_sync || !is_sync {
690                        proj.reason.merge(reason_by_fs());
691                    }
692                }
693            }
694        }
695    }
696
697    /// Apply delayed memory changes to underlying compiler.
698    fn apply_delayed_memory_changes(
699        verse: &mut RevisingVfs<'_, F::AccessModel>,
700        dirty_shadow_logical_tick: &mut usize,
701        event: &Option<UpstreamUpdateEvent>,
702    ) -> Option<()> {
703        // Handle delayed upstream update event before applying file system changes
704        if let Some(event) = event {
705            let TaggedMemoryEvent {
706                logical_tick,
707                event,
708            } = event.opaque.as_ref().downcast_ref()?;
709
710            // Recovery from dirty shadow state.
711            if logical_tick == dirty_shadow_logical_tick {
712                *dirty_shadow_logical_tick = 0;
713            }
714
715            Self::apply_memory_changes(verse, event.clone());
716        }
717
718        Some(())
719    }
720
721    /// Apply memory changes to underlying compiler.
722    fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
723        if matches!(event, MemoryEvent::Sync(..)) {
724            vfs.reset_shadow();
725        }
726        match event {
727            MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
728                for path in event.removes {
729                    let _ = vfs.unmap_shadow(&path);
730                }
731                for (path, snap) in event.inserts {
732                    let _ = vfs.map_shadow(&path, snap);
733                }
734            }
735        }
736    }
737}
738
739/// A project instance state.
740pub struct ProjectInsState<F: CompilerFeat, Ext> {
741    /// The project instance id.
742    pub id: ProjectInsId,
743    /// The extension
744    pub ext: Ext,
745    /// The underlying universe.
746    pub verse: CompilerUniverse<F>,
747    /// Specifies the current export target.
748    pub export_target: ExportTarget,
749    /// The reason to compile.
750    pub reason: CompileSignal,
751    /// The compilation handle.
752    pub handler: Arc<dyn CompileHandler<F, Ext>>,
753    /// The file dependencies.
754    deps: EcoVec<ImmutPath>,
755
756    /// The latest compute graph (snapshot), derived lazily from
757    /// `latest_compilation` as needed.
758    pub cached_snapshot: Option<Arc<WorldComputeGraph<F>>>,
759    /// The latest compilation.
760    pub latest_compilation: OnceLock<CompiledArtifact<F>>,
761    /// The latest successly compiled document.
762    pub latest_success_doc: Option<TypstDocument>,
763
764    committed_revision: usize,
765}
766
767impl<F: CompilerFeat, Ext: 'static> ProjectInsState<F, Ext> {
768    /// Gets a snapshot of the project.
769    pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
770        match self.cached_snapshot.as_ref() {
771            Some(snap) if snap.world().revision() == self.verse.revision => snap.clone(),
772            _ => {
773                let snap = self.make_snapshot();
774                self.cached_snapshot = Some(snap.clone());
775                snap
776            }
777        }
778    }
779
780    /// Creates a new snapshot of the project derived from `latest_compilation`.
781    fn make_snapshot(&self) -> Arc<WorldComputeGraph<F>> {
782        let world = self.verse.snapshot();
783        let snap = CompileSnapshot {
784            id: self.id.clone(),
785            world,
786            signal: self.reason,
787            success_doc: self.latest_success_doc.clone(),
788        };
789        WorldComputeGraph::new(snap)
790    }
791
792    /// Compiles the document once if there is any reason and the entry is
793    /// active. (this is used for experimenting typst.node compilations)
794    #[must_use]
795    pub fn may_compile2<'a>(
796        &mut self,
797        compute: impl FnOnce(&Arc<WorldComputeGraph<F>>) + 'a,
798    ) -> Option<impl FnOnce() -> Arc<WorldComputeGraph<F>> + 'a> {
799        if !self.reason.any() || self.verse.entry_state().is_inactive() {
800            return None;
801        }
802
803        let snap = self.snapshot();
804        self.reason = Default::default();
805        Some(move || {
806            compute(&snap);
807            snap
808        })
809    }
810
811    /// Compiles the document once if there is any reason and the entry is
812    /// active.
813    #[must_use]
814    pub fn may_compile(
815        &mut self,
816        handler: &Arc<dyn CompileHandler<F, Ext>>,
817    ) -> Option<impl FnOnce() -> CompiledArtifact<F> + 'static> {
818        if !self.reason.any() || self.verse.entry_state().is_inactive() {
819            return None;
820        }
821
822        let snap = self.snapshot();
823        self.reason = Default::default();
824
825        Some(Self::run_compile(handler.clone(), snap, self.export_target))
826    }
827
828    /// Compile the document once.
829    fn run_compile(
830        h: Arc<dyn CompileHandler<F, Ext>>,
831        graph: Arc<WorldComputeGraph<F>>,
832        export_target: ExportTarget,
833    ) -> impl FnOnce() -> CompiledArtifact<F> {
834        let start = tinymist_std::time::Instant::now();
835
836        // todo unwrap main id
837        let id = graph.world().main_id().unwrap();
838        let revision = graph.world().revision().get();
839
840        h.status(revision, {
841            CompileReport {
842                id: graph.snap.id.clone(),
843                compiling_id: Some(id),
844                page_count: 0,
845                status: CompileStatusEnum::Compiling,
846            }
847        });
848
849        move || {
850            let compiled =
851                CompiledArtifact::from_graph(graph, matches!(export_target, ExportTarget::Html));
852
853            let res = CompileStatusResult {
854                diag: (compiled.warning_cnt() + compiled.error_cnt()) as u32,
855                elapsed: start.elapsed(),
856            };
857            let rep = CompileReport {
858                id: compiled.id().clone(),
859                compiling_id: Some(id),
860                page_count: compiled.doc.as_ref().map_or(0, |doc| doc.num_of_pages()),
861                status: match &compiled.doc {
862                    Some(..) => CompileStatusEnum::CompileSuccess(res),
863                    None => CompileStatusEnum::CompileError(res),
864                },
865            };
866
867            // todo: we need to check revision for really concurrent compilation
868            log_compile_report(&rep);
869
870            if compiled
871                .diagnostics()
872                .any(|d| d.message == FILE_MISSING_ERROR_MSG)
873            {
874                return compiled;
875            }
876
877            h.status(revision, rep);
878            h.notify_compile(&compiled);
879            compiled
880        }
881    }
882
883    fn process_compile(&mut self, artifact: CompiledArtifact<F>) -> bool {
884        let world = &artifact.snap.world;
885        let compiled_revision = world.revision().get();
886        if self.committed_revision >= compiled_revision {
887            return false;
888        }
889
890        // Updates state.
891        let doc = artifact.doc.clone();
892        self.committed_revision = compiled_revision;
893        if doc.is_some() {
894            self.latest_success_doc = doc;
895        }
896        self.cached_snapshot = None; // invalidate; will be recomputed on demand
897
898        // Notifies the new file dependencies.
899        let mut deps = eco_vec![];
900        world.iter_dependencies(&mut |dep| {
901            if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
902                deps.push(x.into())
903            }
904        });
905
906        self.deps = deps.clone();
907
908        let mut world = world.clone();
909
910        let is_primary = self.id == ProjectInsId("primary".into());
911
912        // Trigger an evict task.
913        spawn_cpu(move || {
914            let evict_start = tinymist_std::time::Instant::now();
915            if is_primary {
916                comemo::evict(10);
917
918                // Since all the projects share the same cache, we need to evict the cache
919                // on the primary instance for all the projects.
920                world.evict_source_cache(30);
921            }
922            world.evict_vfs(60);
923            let elapsed = evict_start.elapsed();
924            log::debug!("ProjectCompiler: evict cache in {elapsed:?}");
925        });
926
927        true
928    }
929}
930
931fn log_compile_report(rep: &CompileReport) {
932    log::info!("{}", rep.message());
933}
934
935#[inline]
936fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
937    res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
938        .is_ok()
939}
940
941#[derive(Debug, Clone, Default)]
942struct ProjectDeps {
943    project_deps: rpds::RedBlackTreeMapSync<ProjectInsId, EcoVec<ImmutPath>>,
944}
945
946impl NotifyDeps for ProjectDeps {
947    fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath)) {
948        for deps in self.project_deps.values().flat_map(|e| e.iter()) {
949            f(deps);
950        }
951    }
952}
953
954// todo: move me to tinymist-std
955#[cfg(not(target_arch = "wasm32"))]
956/// Spawns a CPU thread to run a computing-heavy task.
957pub fn spawn_cpu<F>(func: F)
958where
959    F: FnOnce() + Send + 'static,
960{
961    rayon::spawn(func);
962}
963
964#[cfg(target_arch = "wasm32")]
965/// Spawns a CPU thread to run a computing-heavy task.
966pub fn spawn_cpu<F>(func: F)
967where
968    F: FnOnce() + Send + 'static,
969{
970    func();
971}