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            handler,
413            export_target,
414            deps: Default::default(),
415            latest_compilation: None,
416            cached_snapshot: None,
417        }
418    }
419
420    /// Find a project by id, but with less borrow checker restriction.
421    pub fn find_project<'a>(
422        primary: &'a mut ProjectInsState<F, Ext>,
423        dedicates: &'a mut [ProjectInsState<F, Ext>],
424        id: &ProjectInsId,
425    ) -> &'a mut ProjectInsState<F, Ext> {
426        if id == &primary.id {
427            return primary;
428        }
429
430        dedicates.iter_mut().find(|e| e.id == *id).unwrap()
431    }
432
433    /// Clear all dedicate projects.
434    pub fn clear_dedicates(&mut self) {
435        self.dedicates.clear();
436    }
437
438    /// Restart a dedicate project.
439    pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> Result<ProjectInsId> {
440        let id = ProjectInsId(group.into());
441
442        let verse = CompilerUniverse::<F>::new_raw(
443            entry,
444            self.primary.verse.features.clone(),
445            Some(self.primary.verse.inputs().clone()),
446            self.primary.verse.vfs().fork(),
447            self.primary.verse.registry.clone(),
448            self.primary.verse.font_resolver.clone(),
449            self.primary.verse.creation_timestamp,
450        );
451
452        let mut proj =
453            Self::create_project(id.clone(), verse, self.export_target, self.handler.clone());
454        proj.reason.merge(reason_by_entry_change());
455
456        self.remove_dedicates(&id);
457        self.dedicates.push(proj);
458
459        Ok(id)
460    }
461
462    fn remove_dedicates(&mut self, id: &ProjectInsId) {
463        let proj = self.dedicates.iter().position(|e| e.id == *id);
464        if let Some(idx) = proj {
465            // Resets the handle state, e.g. notified revision
466            self.handler.notify_removed(id);
467            self.deps.project_deps.remove_mut(id);
468
469            let _proj = self.dedicates.remove(idx);
470            // todo: kill compilations
471
472            let res = self
473                .dep_tx
474                .send(NotifyMessage::SyncDependency(Box::new(self.deps.clone())));
475            log_send_error("dep_tx", res);
476        } else {
477            log::warn!("ProjectCompiler: settle project not found {id:?}");
478        }
479    }
480
481    /// Process an interrupt.
482    pub fn process(&mut self, intr: Interrupt<F>) {
483        // todo: evcit cache
484        self.process_inner(intr);
485        // Customized Project Compilation Handler
486        self.handler.clone().on_any_compile_reason(self);
487    }
488
489    fn process_inner(&mut self, intr: Interrupt<F>) {
490        match intr {
491            Interrupt::Compile(id) => {
492                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
493                // Increment the revision anyway.
494                proj.verse.increment_revision(|verse| {
495                    verse.flush();
496                });
497
498                proj.reason.merge(reason_by_entry_change());
499            }
500            Interrupt::Compiled(artifact) => {
501                let proj =
502                    Self::find_project(&mut self.primary, &mut self.dedicates, artifact.id());
503
504                let processed = proj.process_compile(artifact);
505
506                if processed {
507                    self.deps
508                        .project_deps
509                        .insert_mut(proj.id.clone(), proj.deps.clone());
510
511                    let event = NotifyMessage::SyncDependency(Box::new(self.deps.clone()));
512                    let err = self.dep_tx.send(event);
513                    log_send_error("dep_tx", err);
514                }
515            }
516            Interrupt::Settle(id) => {
517                self.remove_dedicates(&id);
518            }
519            Interrupt::ChangeTask(id, change) => {
520                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
521                proj.verse.increment_revision(|verse| {
522                    if let Some(inputs) = change.inputs.clone() {
523                        verse.set_inputs(inputs);
524                    }
525
526                    if let Some(entry) = change.entry.clone() {
527                        let res = verse.mutate_entry(entry);
528                        if let Err(err) = res {
529                            log::error!("ProjectCompiler: change entry error: {err:?}");
530                        }
531                    }
532                });
533
534                // After incrementing the revision
535                if let Some(entry) = change.entry {
536                    // todo: dedicate suspended
537                    if entry.is_inactive() {
538                        log::info!("ProjectCompiler: removing diag");
539                        self.handler.status(proj.verse.revision.get(), {
540                            CompileReport {
541                                id: proj.id.clone(),
542                                compiling_id: None,
543                                page_count: 0,
544                                status: CompileStatusEnum::Suspend,
545                            }
546                        });
547                    }
548
549                    // Forget the old compilation state.
550                    proj.latest_compilation = None;
551                }
552
553                proj.reason.merge(reason_by_entry_change());
554            }
555
556            Interrupt::Font(fonts) => {
557                self.projects().for_each(|proj| {
558                    let font_changed = proj.verse.increment_revision(|verse| {
559                        verse.set_fonts(fonts.clone());
560                        verse.font_changed()
561                    });
562                    if font_changed {
563                        // todo: reason_by_font_change
564                        proj.reason.merge(reason_by_entry_change());
565                    }
566                });
567            }
568            Interrupt::CreationTimestamp(creation_timestamp) => {
569                self.projects().for_each(|proj| {
570                    let timestamp_changed = proj.verse.increment_revision(|verse| {
571                        verse.set_creation_timestamp(creation_timestamp);
572                        // Creation timestamp changes affect compilation
573                        verse.creation_timestamp_changed()
574                    });
575                    if timestamp_changed {
576                        proj.reason.merge(reason_by_entry_change());
577                    }
578                });
579            }
580            Interrupt::Memory(event) => {
581                log::debug!("ProjectCompiler: memory event incoming");
582
583                // Emulate memory changes.
584                let mut files = HashSet::new();
585                if matches!(event, MemoryEvent::Sync(..)) {
586                    std::mem::swap(&mut files, &mut self.estimated_shadow_files);
587                }
588
589                let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
590                for path in &e.removes {
591                    self.estimated_shadow_files.remove(path);
592                    files.insert(Arc::clone(path));
593                }
594                for (path, _) in &e.inserts {
595                    self.estimated_shadow_files.insert(Arc::clone(path));
596                    files.remove(path);
597                }
598
599                // If there is no invalidation happening, apply memory changes directly.
600                if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
601                    let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
602                    let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
603                    for (proj, event) in proj.zip(changes) {
604                        log::debug!("memory update: vfs {:#?}", proj.verse.vfs().display());
605                        let vfs_changed = proj.verse.increment_revision(|verse| {
606                            log::debug!("memory update: {:?}", proj.id);
607                            Self::apply_memory_changes(&mut verse.vfs(), event.clone());
608                            log::debug!("memory update: changed {}", verse.vfs_changed());
609                            verse.vfs_changed()
610                        });
611                        if vfs_changed {
612                            proj.reason.merge(reason_by_mem());
613                        }
614                        log::debug!("memory update: vfs after {:#?}", proj.verse.vfs().display());
615                    }
616                    return;
617                }
618
619                // Otherwise, send upstream update event.
620                // Also, record the logical tick when shadow is dirty.
621                self.dirty_shadow_logical_tick = self.logical_tick;
622                let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
623                    invalidates: files.into_iter().collect(),
624                    opaque: Box::new(TaggedMemoryEvent {
625                        logical_tick: self.logical_tick,
626                        event,
627                    }),
628                });
629                let err = self.dep_tx.send(event);
630                log_send_error("dep_tx", err);
631            }
632            Interrupt::Save(event) => {
633                let changes = std::iter::repeat_n(&event, 1 + self.dedicates.len());
634                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
635
636                for (proj, saved_path) in proj.zip(changes) {
637                    log::debug!(
638                        "ProjectCompiler({}, rev={}): save changes",
639                        proj.verse.revision.get(),
640                        proj.id
641                    );
642
643                    // todo: only emit if saved_path is related
644                    let _ = saved_path;
645
646                    proj.reason.merge(reason_by_fs());
647                }
648            }
649            Interrupt::Fs(event) => {
650                log::debug!("ProjectCompiler: fs event incoming {event:?}");
651
652                // Apply file system changes.
653                let dirty_tick = &mut self.dirty_shadow_logical_tick;
654                let (changes, is_sync, event) = event.split_with_is_sync();
655                let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
656                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
657
658                for (proj, changes) in proj.zip(changes) {
659                    log::debug!(
660                        "ProjectCompiler({}, rev={}): fs changes applying",
661                        proj.verse.revision.get(),
662                        proj.id
663                    );
664
665                    proj.verse.increment_revision(|verse| {
666                        let mut vfs = verse.vfs();
667
668                        // Handle delayed upstream update event before applying file system
669                        // changes
670                        if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
671                            .is_none()
672                        {
673                            log::warn!("ProjectCompiler: unknown upstream update event");
674
675                            // Actual a delayed memory event.
676                            proj.reason.merge(reason_by_mem());
677                        }
678                        vfs.notify_fs_changes(changes);
679                    });
680
681                    log::debug!(
682                        "ProjectCompiler({},rev={}): fs changes applied, {is_sync}",
683                        proj.id,
684                        proj.verse.revision.get(),
685                    );
686
687                    if !self.ignore_first_sync || !is_sync {
688                        proj.reason.merge(reason_by_fs());
689                    }
690                }
691            }
692        }
693    }
694
695    /// Apply delayed memory changes to underlying compiler.
696    fn apply_delayed_memory_changes(
697        verse: &mut RevisingVfs<'_, F::AccessModel>,
698        dirty_shadow_logical_tick: &mut usize,
699        event: &Option<UpstreamUpdateEvent>,
700    ) -> Option<()> {
701        // Handle delayed upstream update event before applying file system changes
702        if let Some(event) = event {
703            let TaggedMemoryEvent {
704                logical_tick,
705                event,
706            } = event.opaque.as_ref().downcast_ref()?;
707
708            // Recovery from dirty shadow state.
709            if logical_tick == dirty_shadow_logical_tick {
710                *dirty_shadow_logical_tick = 0;
711            }
712
713            Self::apply_memory_changes(verse, event.clone());
714        }
715
716        Some(())
717    }
718
719    /// Apply memory changes to underlying compiler.
720    fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
721        if matches!(event, MemoryEvent::Sync(..)) {
722            vfs.reset_shadow();
723        }
724        match event {
725            MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
726                for path in event.removes {
727                    let _ = vfs.unmap_shadow(&path);
728                }
729                for (path, snap) in event.inserts {
730                    let _ = vfs.map_shadow(&path, snap);
731                }
732            }
733        }
734    }
735}
736
737/// A project instance state.
738pub struct ProjectInsState<F: CompilerFeat, Ext> {
739    /// The project instance id.
740    pub id: ProjectInsId,
741    /// The extension
742    pub ext: Ext,
743    /// The underlying universe.
744    pub verse: CompilerUniverse<F>,
745    /// Specifies the current export target.
746    pub export_target: ExportTarget,
747    /// The reason to compile.
748    pub reason: CompileSignal,
749    /// The compilation handle.
750    pub handler: Arc<dyn CompileHandler<F, Ext>>,
751    /// The file dependencies.
752    deps: EcoVec<ImmutPath>,
753
754    latest_compilation: Option<CompilationState>,
755    /// The latest compute graph (snapshot), derived lazily from
756    /// `latest_compilation` as needed.
757    cached_snapshot: Option<Arc<WorldComputeGraph<F>>>,
758}
759
760/// Information about a completed compilation.
761struct CompilationState {
762    revision: usize,
763    /// The document, if it compiled successfully.
764    doc: Option<TypstDocument>,
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        // Tries to use the cached snapshot if possible.
771        match self.cached_snapshot.as_ref() {
772            Some(cached) if cached.world().revision() == self.verse.revision => cached.clone(),
773            _ => {
774                let snap = self.make_snapshot();
775                self.cached_snapshot = Some(snap.clone());
776                snap
777            }
778        }
779    }
780
781    /// Creates a new snapshot of the project derived from `latest_compilation`.
782    fn make_snapshot(&self) -> Arc<WorldComputeGraph<F>> {
783        let world = self.verse.snapshot();
784        let snap = CompileSnapshot {
785            id: self.id.clone(),
786            world,
787            signal: self.reason,
788            success_doc: self.latest_compilation.as_ref().and_then(|c| c.doc.clone()),
789        };
790        WorldComputeGraph::new(snap)
791    }
792
793    /// Compiles the document once if there is any reason and the entry is
794    /// active. (this is used for experimenting typst.node compilations)
795    #[must_use]
796    pub fn may_compile2<'a>(
797        &mut self,
798        compute: impl FnOnce(&Arc<WorldComputeGraph<F>>) + 'a,
799    ) -> Option<impl FnOnce() -> Arc<WorldComputeGraph<F>> + 'a> {
800        if !self.reason.any() || self.verse.entry_state().is_inactive() {
801            return None;
802        }
803
804        let snap = self.snapshot();
805        self.reason = Default::default();
806        Some(move || {
807            compute(&snap);
808            snap
809        })
810    }
811
812    /// Compiles the document once if there is any reason and the entry is
813    /// active.
814    #[must_use]
815    pub fn may_compile(
816        &mut self,
817        handler: &Arc<dyn CompileHandler<F, Ext>>,
818    ) -> Option<impl FnOnce() -> CompiledArtifact<F> + 'static> {
819        if !self.reason.any() || self.verse.entry_state().is_inactive() {
820            return None;
821        }
822
823        let snap = self.snapshot();
824        self.reason = Default::default();
825
826        Some(Self::run_compile(handler.clone(), snap, self.export_target))
827    }
828
829    /// Compile the document once.
830    fn run_compile(
831        h: Arc<dyn CompileHandler<F, Ext>>,
832        graph: Arc<WorldComputeGraph<F>>,
833        export_target: ExportTarget,
834    ) -> impl FnOnce() -> CompiledArtifact<F> {
835        let start = tinymist_std::time::Instant::now();
836
837        // todo unwrap main id
838        let id = graph.world().main_id().unwrap();
839        let revision = graph.world().revision().get();
840
841        h.status(revision, {
842            CompileReport {
843                id: graph.snap.id.clone(),
844                compiling_id: Some(id),
845                page_count: 0,
846                status: CompileStatusEnum::Compiling,
847            }
848        });
849
850        move || {
851            let compiled =
852                CompiledArtifact::from_graph(graph, matches!(export_target, ExportTarget::Html));
853
854            let res = CompileStatusResult {
855                diag: (compiled.warning_cnt() + compiled.error_cnt()) as u32,
856                elapsed: start.elapsed(),
857            };
858            let rep = CompileReport {
859                id: compiled.id().clone(),
860                compiling_id: Some(id),
861                page_count: compiled.doc.as_ref().map_or(0, |doc| doc.num_of_pages()),
862                status: match &compiled.doc {
863                    Some(..) => CompileStatusEnum::CompileSuccess(res),
864                    None => CompileStatusEnum::CompileError(res),
865                },
866            };
867
868            // todo: we need to check revision for really concurrent compilation
869            log_compile_report(&rep);
870
871            if compiled
872                .diagnostics()
873                .any(|d| d.message == FILE_MISSING_ERROR_MSG)
874            {
875                return compiled;
876            }
877
878            h.status(revision, rep);
879            h.notify_compile(&compiled);
880            compiled
881        }
882    }
883
884    fn process_compile(&mut self, artifact: CompiledArtifact<F>) -> bool {
885        let world = &artifact.snap.world;
886        let compiled_revision = world.revision().get();
887        if let Some(cur) = &self.latest_compilation
888            && cur.revision >= compiled_revision
889        {
890            return false;
891        }
892
893        // Updates state.
894        let doc = artifact.doc.clone();
895        self.latest_compilation = Some(CompilationState {
896            revision: compiled_revision,
897            doc,
898        });
899        // Invalidates the snapshot. It will be recomputed on demand.
900        self.cached_snapshot = None;
901
902        // Notifies the new file dependencies.
903        let mut deps = eco_vec![];
904        world.iter_dependencies(&mut |dep| {
905            if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
906                deps.push(x.into())
907            }
908        });
909
910        self.deps = deps.clone();
911
912        let mut world = world.clone();
913
914        let is_primary = self.id == ProjectInsId("primary".into());
915
916        // Trigger an evict task.
917        spawn_cpu(move || {
918            let evict_start = tinymist_std::time::Instant::now();
919            if is_primary {
920                comemo::evict(10);
921
922                // Since all the projects share the same cache, we need to evict the cache
923                // on the primary instance for all the projects.
924                world.evict_source_cache(30);
925            }
926            world.evict_vfs(60);
927            let elapsed = evict_start.elapsed();
928            log::debug!("ProjectCompiler: evict cache in {elapsed:?}");
929        });
930
931        true
932    }
933}
934
935fn log_compile_report(rep: &CompileReport) {
936    log::info!("{}", rep.message());
937}
938
939#[inline]
940fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
941    res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
942        .is_ok()
943}
944
945#[derive(Debug, Clone, Default)]
946struct ProjectDeps {
947    project_deps: rpds::RedBlackTreeMapSync<ProjectInsId, EcoVec<ImmutPath>>,
948}
949
950impl NotifyDeps for ProjectDeps {
951    fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath)) {
952        for deps in self.project_deps.values().flat_map(|e| e.iter()) {
953            f(deps);
954        }
955    }
956}
957
958// todo: move me to tinymist-std
959#[cfg(not(target_arch = "wasm32"))]
960/// Spawns a CPU thread to run a computing-heavy task.
961pub fn spawn_cpu<F>(func: F)
962where
963    F: FnOnce() + Send + 'static,
964{
965    rayon::spawn(func);
966}
967
968#[cfg(target_arch = "wasm32")]
969/// Spawns a CPU thread to run a computing-heavy task.
970pub fn spawn_cpu<F>(func: F)
971where
972    F: FnOnce() + Send + 'static,
973{
974    func();
975}