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::World;
23use typst::diag::{At, FileError};
24use typst::syntax::Span;
25
26/// A compiled artifact.
27pub struct CompiledArtifact<F: CompilerFeat> {
28    /// The used compute graph.
29    pub graph: Arc<WorldComputeGraph<F>>,
30    /// The diagnostics of the document.
31    pub diag: Arc<DiagnosticsTask>,
32    /// The compiled document.
33    pub doc: Option<TypstDocument>,
34    /// The depended files.
35    pub deps: OnceLock<EcoVec<FileId>>,
36}
37
38impl<F: CompilerFeat> fmt::Display for CompiledArtifact<F> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        let rev = self.graph.snap.world.revision();
41        write!(f, "CompiledArtifact({:?}, rev={rev:?})", self.graph.snap.id)
42    }
43}
44
45impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
46    type Target = Arc<WorldComputeGraph<F>>;
47
48    fn deref(&self) -> &Self::Target {
49        &self.graph
50    }
51}
52
53impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
54    fn clone(&self) -> Self {
55        Self {
56            graph: self.graph.clone(),
57            doc: self.doc.clone(),
58            diag: self.diag.clone(),
59            deps: self.deps.clone(),
60        }
61    }
62}
63
64impl<F: CompilerFeat> CompiledArtifact<F> {
65    /// Returns the project id.
66    pub fn id(&self) -> &ProjectInsId {
67        &self.graph.snap.id
68    }
69
70    /// Returns the last successfully compiled document.
71    pub fn success_doc(&self) -> Option<TypstDocument> {
72        self.doc
73            .as_ref()
74            .cloned()
75            .or_else(|| self.snap.success_doc.clone())
76    }
77
78    /// Returns the depended files.
79    pub fn depended_files(&self) -> &EcoVec<FileId> {
80        self.deps.get_or_init(|| {
81            let mut deps = EcoVec::default();
82            self.graph.snap.world.iter_dependencies(&mut |f| {
83                deps.push(f);
84            });
85
86            deps
87        })
88    }
89
90    /// Runs the compiler and returns the compiled document.
91    pub fn from_graph(graph: Arc<WorldComputeGraph<F>>, is_html: bool) -> CompiledArtifact<F> {
92        let _ = graph.provide::<FlagTask<HtmlCompilationTask>>(Ok(FlagTask::flag(is_html)));
93        let _ = graph.provide::<FlagTask<PagedCompilationTask>>(Ok(FlagTask::flag(!is_html)));
94        let doc = if is_html {
95            graph.shared_compile_html().expect("html").map(From::from)
96        } else {
97            graph.shared_compile().expect("paged").map(From::from)
98        };
99
100        CompiledArtifact {
101            diag: graph.shared_diagnostics().expect("diag"),
102            graph,
103            doc,
104            deps: OnceLock::default(),
105        }
106    }
107
108    /// Returns the error count.
109    pub fn error_cnt(&self) -> usize {
110        self.diag.error_cnt()
111    }
112
113    /// Returns the warning count.
114    pub fn warning_cnt(&self) -> usize {
115        self.diag.warning_cnt()
116    }
117
118    /// Returns the diagnostics.
119    pub fn diagnostics(&self) -> impl Iterator<Item = &typst::diag::SourceDiagnostic> + Clone {
120        self.diag.diagnostics()
121    }
122
123    /// Returns whether there are any errors.
124    pub fn has_errors(&self) -> bool {
125        self.error_cnt() > 0
126    }
127
128    /// Sets the signal.
129    pub fn with_signal(mut self, signal: CompileSignal) -> Self {
130        let mut snap = self.snap.clone();
131        snap.signal = signal;
132
133        self.graph = self.graph.snapshot_unsafe(snap);
134        self
135    }
136}
137
138/// The compilation status of a project.
139#[derive(Debug, Clone)]
140pub struct CompileReport {
141    /// The project ID.
142    pub id: ProjectInsId,
143    /// The file getting compiled.
144    pub compiling_id: Option<FileId>,
145    /// The number of pages in the compiled document, zero if failed.
146    pub page_count: u32,
147    /// The status of the compilation.
148    pub status: CompileStatusEnum,
149}
150
151/// The compilation status of a project.
152#[derive(Debug, Clone)]
153pub enum CompileStatusEnum {
154    /// The project is suspended.
155    Suspend,
156    /// The project is compiling.
157    Compiling,
158    /// The project compiled successfully.
159    CompileSuccess(CompileStatusResult),
160    /// The project failed to compile.
161    CompileError(CompileStatusResult),
162    /// The project failed to export.
163    ExportError(CompileStatusResult),
164}
165
166/// The compilation status result of a project.
167#[derive(Debug, Clone)]
168pub struct CompileStatusResult {
169    /// The number of errors or warnings occur.
170    diag: u32,
171    /// Used time
172    elapsed: tinymist_std::time::Duration,
173}
174
175impl CompileReport {
176    /// Gets the status message.
177    pub fn message(&self) -> CompileReportMsg<'_> {
178        CompileReportMsg(self)
179    }
180}
181
182/// A message of the compilation status.
183pub struct CompileReportMsg<'a>(&'a CompileReport);
184
185impl fmt::Display for CompileReportMsg<'_> {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        use CompileStatusEnum::*;
188        use CompileStatusResult as Res;
189
190        let input = WorkspaceResolver::display(self.0.compiling_id);
191        let (stage, Res { diag, elapsed }) = match &self.0.status {
192            Suspend => return f.write_str("suspended"),
193            Compiling => return f.write_str("compiling"),
194            CompileSuccess(Res { diag: 0, elapsed }) => {
195                return write!(f, "{input:?}: compilation succeeded in {elapsed:?}");
196            }
197            CompileSuccess(res) => ("compilation succeeded", res),
198            CompileError(res) => ("compilation failed", res),
199            ExportError(res) => ("export failed", res),
200        };
201        write!(
202            f,
203            "{input:?}: {stage} with {diag} warnings and errors in {elapsed:?}"
204        )
205    }
206}
207
208/// A project compiler handler.
209pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
210    /// Called when there is any reason to compile. This doesn't mean that the
211    /// project should be compiled.
212    fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
213    // todo: notify project specific compile
214    /// Called when a compilation is done.
215    fn notify_compile(&self, res: &CompiledArtifact<F>);
216    /// Called when a project is removed.
217    fn notify_removed(&self, _id: &ProjectInsId) {}
218    /// Called when the compilation status is changed.
219    fn status(&self, revision: usize, rep: CompileReport);
220}
221
222/// No need so no compilation.
223impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> CompileHandler<F, Ext>
224    for std::marker::PhantomData<fn(F, Ext)>
225{
226    fn on_any_compile_reason(&self, _state: &mut ProjectCompiler<F, Ext>) {
227        log::info!("ProjectHandle: no need to compile");
228    }
229    fn notify_compile(&self, _res: &CompiledArtifact<F>) {}
230    fn status(&self, _revision: usize, _rep: CompileReport) {}
231}
232
233/// An interrupt to the compiler.
234pub enum Interrupt<F: CompilerFeat> {
235    /// Compile anyway.
236    Compile(ProjectInsId),
237    /// Settle a dedicated project.
238    Settle(ProjectInsId),
239    /// Compiled from computing thread.
240    Compiled(CompiledArtifact<F>),
241    /// Change the watching entry.
242    ChangeTask(ProjectInsId, TaskInputs),
243    /// Font changes.
244    Font(Arc<F::FontResolver>),
245    /// Creation timestamp changes.
246    CreationTimestamp(Option<i64>),
247    /// Memory file changes.
248    Memory(MemoryEvent),
249    /// File system event.
250    Fs(FilesystemEvent),
251    /// Save a file.
252    Save(ImmutPath),
253}
254
255impl<F: CompilerFeat> fmt::Debug for Interrupt<F> {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Interrupt::Compile(id) => write!(f, "Compile({id:?})"),
259            Interrupt::Settle(id) => write!(f, "Settle({id:?})"),
260            Interrupt::Compiled(artifact) => write!(f, "Compiled({:?})", artifact.id()),
261            Interrupt::ChangeTask(id, change) => {
262                write!(f, "ChangeTask({id:?}, entry={:?})", change.entry.is_some())
263            }
264            Interrupt::Font(..) => write!(f, "Font(..)"),
265            Interrupt::CreationTimestamp(ts) => write!(f, "CreationTimestamp({ts:?})"),
266            Interrupt::Memory(..) => write!(f, "Memory(..)"),
267            Interrupt::Fs(..) => write!(f, "Fs(..)"),
268            Interrupt::Save(path) => write!(f, "Save({path:?})"),
269        }
270    }
271}
272
273fn no_reason() -> CompileSignal {
274    CompileSignal::default()
275}
276
277fn reason_by_mem() -> CompileSignal {
278    CompileSignal {
279        by_mem_events: true,
280        ..CompileSignal::default()
281    }
282}
283
284fn reason_by_fs() -> CompileSignal {
285    CompileSignal {
286        by_fs_events: true,
287        ..CompileSignal::default()
288    }
289}
290
291fn reason_by_entry_change() -> CompileSignal {
292    CompileSignal {
293        by_entry_update: true,
294        ..CompileSignal::default()
295    }
296}
297
298/// A tagged memory event with logical tick.
299struct TaggedMemoryEvent {
300    /// The logical tick when the event is received.
301    logical_tick: usize,
302    /// The memory event happened.
303    event: MemoryEvent,
304}
305
306/// The compiler server options.
307pub struct CompileServerOpts<F: CompilerFeat, Ext> {
308    /// The compilation handler.
309    pub handler: Arc<dyn CompileHandler<F, Ext>>,
310    /// Whether to ignoring the first fs sync event.
311    pub ignore_first_sync: bool,
312    /// Specifies the current export target.
313    pub export_target: ExportTarget,
314    /// Whether to run in syntax-only mode.
315    pub syntax_only: bool,
316}
317
318impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> Default for CompileServerOpts<F, Ext> {
319    fn default() -> Self {
320        Self {
321            handler: Arc::new(std::marker::PhantomData),
322            ignore_first_sync: false,
323            syntax_only: false,
324            export_target: ExportTarget::Paged,
325        }
326    }
327}
328
329const FILE_MISSING_ERROR_MSG: EcoString = EcoString::inline("t-file-missing");
330/// The file missing error constant.
331pub const FILE_MISSING_ERROR: FileError = FileError::Other(Some(FILE_MISSING_ERROR_MSG));
332
333/// The synchronous compiler that runs on one project or multiple projects.
334pub struct ProjectCompiler<F: CompilerFeat, Ext> {
335    /// The compilation handle.
336    pub handler: Arc<dyn CompileHandler<F, Ext>>,
337    /// Specifies the current export target.
338    export_target: ExportTarget,
339    /// Whether to run in syntax-only mode.
340    syntax_only: bool,
341    /// Channel for sending interrupts to the compiler actor.
342    dep_tx: mpsc::UnboundedSender<NotifyMessage>,
343    /// Whether to ignore the first sync event.
344    pub ignore_first_sync: bool,
345
346    /// The current logical tick.
347    logical_tick: usize,
348    /// Last logical tick when invalidation is caused by shadow update.
349    dirty_shadow_logical_tick: usize,
350    /// Estimated latest set of shadow files.
351    estimated_shadow_files: HashSet<Arc<Path>>,
352
353    /// The primary state.
354    pub primary: ProjectInsState<F, Ext>,
355    /// The states for dedicate tasks
356    pub dedicates: Vec<ProjectInsState<F, Ext>>,
357    /// The project file dependencies.
358    deps: ProjectDeps,
359}
360
361impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
362    /// Creates a compiler with options
363    pub fn new(
364        verse: CompilerUniverse<F>,
365        dep_tx: mpsc::UnboundedSender<NotifyMessage>,
366        CompileServerOpts {
367            handler,
368            ignore_first_sync,
369            export_target,
370            syntax_only,
371        }: CompileServerOpts<F, Ext>,
372    ) -> Self {
373        let primary = Self::create_project(
374            ProjectInsId("primary".into()),
375            verse,
376            export_target,
377            syntax_only,
378            handler.clone(),
379        );
380        Self {
381            handler,
382            dep_tx,
383            export_target,
384            syntax_only,
385
386            logical_tick: 1,
387            dirty_shadow_logical_tick: 0,
388
389            estimated_shadow_files: Default::default(),
390            ignore_first_sync,
391
392            primary,
393            deps: Default::default(),
394            dedicates: vec![],
395        }
396    }
397
398    /// Creates a snapshot of the primary project.
399    pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
400        self.primary.snapshot()
401    }
402
403    /// Compiles the document once.
404    pub fn compile_once(&mut self) -> CompiledArtifact<F> {
405        let snap = self.primary.make_snapshot();
406        ProjectInsState::run_compile(
407            self.handler.clone(),
408            snap,
409            self.export_target,
410            self.syntax_only,
411        )()
412    }
413
414    /// Gets the iterator of all projects.
415    pub fn projects(&mut self) -> impl Iterator<Item = &mut ProjectInsState<F, Ext>> {
416        std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut())
417    }
418
419    fn create_project(
420        id: ProjectInsId,
421        verse: CompilerUniverse<F>,
422        export_target: ExportTarget,
423        syntax_only: bool,
424        handler: Arc<dyn CompileHandler<F, Ext>>,
425    ) -> ProjectInsState<F, Ext> {
426        ProjectInsState {
427            id,
428            ext: Default::default(),
429            syntax_only,
430            verse,
431            reason: no_reason(),
432            cached_snapshot: None,
433            handler,
434            export_target,
435            latest_compilation: OnceLock::default(),
436            latest_success_doc: None,
437            deps: Default::default(),
438            committed_revision: 0,
439        }
440    }
441
442    /// Find a project by id, but with less borrow checker restriction.
443    pub fn find_project<'a>(
444        primary: &'a mut ProjectInsState<F, Ext>,
445        dedicates: &'a mut [ProjectInsState<F, Ext>],
446        id: &ProjectInsId,
447    ) -> &'a mut ProjectInsState<F, Ext> {
448        if id == &primary.id {
449            return primary;
450        }
451
452        dedicates.iter_mut().find(|e| e.id == *id).unwrap()
453    }
454
455    /// Clear all dedicate projects.
456    pub fn clear_dedicates(&mut self) {
457        self.dedicates.clear();
458    }
459
460    /// Restart a dedicate project.
461    pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> Result<ProjectInsId> {
462        let id = ProjectInsId(group.into());
463
464        let verse = CompilerUniverse::<F>::new_raw(
465            entry,
466            self.primary.verse.features.clone(),
467            Some(self.primary.verse.inputs().clone()),
468            self.primary.verse.vfs().fork(),
469            self.primary.verse.registry.clone(),
470            self.primary.verse.font_resolver.clone(),
471            self.primary.verse.creation_timestamp,
472        );
473
474        let mut proj = Self::create_project(
475            id.clone(),
476            verse,
477            self.export_target,
478            self.syntax_only,
479            self.handler.clone(),
480        );
481        proj.reason.merge(reason_by_entry_change());
482
483        self.remove_dedicates(&id);
484        self.dedicates.push(proj);
485
486        Ok(id)
487    }
488
489    fn remove_dedicates(&mut self, id: &ProjectInsId) {
490        let proj = self.dedicates.iter().position(|e| e.id == *id);
491        if let Some(idx) = proj {
492            // Resets the handle state, e.g. notified revision
493            self.handler.notify_removed(id);
494            self.deps.project_deps.remove_mut(id);
495
496            let _proj = self.dedicates.remove(idx);
497            // todo: kill compilations
498
499            let res = self
500                .dep_tx
501                .send(NotifyMessage::SyncDependency(Box::new(self.deps.clone())));
502            log_send_error("dep_tx", res);
503        } else {
504            log::warn!("ProjectCompiler: settle project not found {id:?}");
505        }
506    }
507
508    /// Process an interrupt.
509    pub fn process(&mut self, intr: Interrupt<F>) {
510        // todo: evcit cache
511        self.process_inner(intr);
512        // Customized Project Compilation Handler
513        self.handler.clone().on_any_compile_reason(self);
514    }
515
516    fn process_inner(&mut self, intr: Interrupt<F>) {
517        match intr {
518            Interrupt::Compile(id) => {
519                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
520                // Increment the revision anyway.
521                proj.verse.increment_revision(|verse| {
522                    verse.flush();
523                });
524
525                proj.reason.merge(reason_by_entry_change());
526            }
527            Interrupt::Compiled(artifact) => {
528                let proj =
529                    Self::find_project(&mut self.primary, &mut self.dedicates, artifact.id());
530
531                let processed = proj.process_compile(artifact);
532
533                if processed {
534                    self.deps
535                        .project_deps
536                        .insert_mut(proj.id.clone(), proj.deps.clone());
537
538                    let event = NotifyMessage::SyncDependency(Box::new(self.deps.clone()));
539                    let err = self.dep_tx.send(event);
540                    log_send_error("dep_tx", err);
541                }
542            }
543            Interrupt::Settle(id) => {
544                self.remove_dedicates(&id);
545            }
546            Interrupt::ChangeTask(id, change) => {
547                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
548                proj.verse.increment_revision(|verse| {
549                    if let Some(inputs) = change.inputs.clone() {
550                        verse.set_inputs(inputs);
551                    }
552
553                    if let Some(entry) = change.entry.clone() {
554                        let res = verse.mutate_entry(entry);
555                        if let Err(err) = res {
556                            log::error!("ProjectCompiler: change entry error: {err:?}");
557                        }
558                    }
559                });
560
561                // After incrementing the revision
562                if let Some(entry) = change.entry {
563                    // todo: dedicate suspended
564                    if entry.is_inactive() {
565                        log::info!("ProjectCompiler: removing diag");
566                        self.handler.status(proj.verse.revision.get(), {
567                            CompileReport {
568                                id: proj.id.clone(),
569                                compiling_id: None,
570                                page_count: 0,
571                                status: CompileStatusEnum::Suspend,
572                            }
573                        });
574                    }
575
576                    // Forget the document state of previous entry.
577                    proj.latest_success_doc = None;
578                }
579
580                proj.reason.merge(reason_by_entry_change());
581            }
582
583            Interrupt::Font(fonts) => {
584                self.projects().for_each(|proj| {
585                    let font_changed = proj.verse.increment_revision(|verse| {
586                        verse.set_fonts(fonts.clone());
587                        verse.font_changed()
588                    });
589                    if font_changed {
590                        // todo: reason_by_font_change
591                        proj.reason.merge(reason_by_entry_change());
592                    }
593                });
594            }
595            Interrupt::CreationTimestamp(creation_timestamp) => {
596                self.projects().for_each(|proj| {
597                    let timestamp_changed = proj.verse.increment_revision(|verse| {
598                        verse.set_creation_timestamp(creation_timestamp);
599                        // Creation timestamp changes affect compilation
600                        verse.creation_timestamp_changed()
601                    });
602                    if timestamp_changed {
603                        proj.reason.merge(reason_by_entry_change());
604                    }
605                });
606            }
607            Interrupt::Memory(event) => {
608                log::debug!("ProjectCompiler: memory event incoming");
609
610                // Emulate memory changes.
611                let mut files = HashSet::new();
612                if matches!(event, MemoryEvent::Sync(..)) {
613                    std::mem::swap(&mut files, &mut self.estimated_shadow_files);
614                }
615
616                let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
617                for path in &e.removes {
618                    self.estimated_shadow_files.remove(path);
619                    files.insert(Arc::clone(path));
620                }
621                for (path, _) in &e.inserts {
622                    self.estimated_shadow_files.insert(Arc::clone(path));
623                    files.remove(path);
624                }
625
626                // If there is no invalidation happening, apply memory changes directly.
627                if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
628                    let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
629                    let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
630                    for (proj, event) in proj.zip(changes) {
631                        log::debug!("memory update: vfs {:#?}", proj.verse.vfs().display());
632                        let vfs_changed = proj.verse.increment_revision(|verse| {
633                            log::debug!("memory update: {:?}", proj.id);
634                            Self::apply_memory_changes(&mut verse.vfs(), event.clone());
635                            log::debug!("memory update: changed {}", verse.vfs_changed());
636                            verse.vfs_changed()
637                        });
638                        if vfs_changed {
639                            proj.reason.merge(reason_by_mem());
640                        }
641                        log::debug!("memory update: vfs after {:#?}", proj.verse.vfs().display());
642                    }
643                    return;
644                }
645
646                // Otherwise, send upstream update event.
647                // Also, record the logical tick when shadow is dirty.
648                self.dirty_shadow_logical_tick = self.logical_tick;
649                let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
650                    invalidates: files.into_iter().collect(),
651                    opaque: Box::new(TaggedMemoryEvent {
652                        logical_tick: self.logical_tick,
653                        event,
654                    }),
655                });
656                let err = self.dep_tx.send(event);
657                log_send_error("dep_tx", err);
658            }
659            Interrupt::Save(event) => {
660                let changes = std::iter::repeat_n(&event, 1 + self.dedicates.len());
661                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
662
663                for (proj, saved_path) in proj.zip(changes) {
664                    log::debug!(
665                        "ProjectCompiler({}, rev={}): save changes",
666                        proj.verse.revision.get(),
667                        proj.id
668                    );
669
670                    // todo: only emit if saved_path is related
671                    let _ = saved_path;
672
673                    proj.reason.merge(reason_by_fs());
674                }
675            }
676            Interrupt::Fs(event) => {
677                log::debug!("ProjectCompiler: fs event incoming {event:?}");
678
679                // Apply file system changes.
680                let dirty_tick = &mut self.dirty_shadow_logical_tick;
681                let (changes, is_sync, event) = event.split_with_is_sync();
682                let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
683                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
684
685                for (proj, changes) in proj.zip(changes) {
686                    log::debug!(
687                        "ProjectCompiler({}, rev={}): fs changes applying",
688                        proj.verse.revision.get(),
689                        proj.id
690                    );
691
692                    proj.verse.increment_revision(|verse| {
693                        let mut vfs = verse.vfs();
694
695                        // Handle delayed upstream update event before applying file system
696                        // changes
697                        if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
698                            .is_none()
699                        {
700                            log::warn!("ProjectCompiler: unknown upstream update event");
701
702                            // Actual a delayed memory event.
703                            proj.reason.merge(reason_by_mem());
704                        }
705                        vfs.notify_fs_changes(changes);
706                    });
707
708                    log::debug!(
709                        "ProjectCompiler({},rev={}): fs changes applied, {is_sync}",
710                        proj.id,
711                        proj.verse.revision.get(),
712                    );
713
714                    if !self.ignore_first_sync || !is_sync {
715                        proj.reason.merge(reason_by_fs());
716                    }
717                }
718            }
719        }
720    }
721
722    /// Apply delayed memory changes to underlying compiler.
723    fn apply_delayed_memory_changes(
724        verse: &mut RevisingVfs<'_, F::AccessModel>,
725        dirty_shadow_logical_tick: &mut usize,
726        event: &Option<UpstreamUpdateEvent>,
727    ) -> Option<()> {
728        // Handle delayed upstream update event before applying file system changes
729        if let Some(event) = event {
730            let TaggedMemoryEvent {
731                logical_tick,
732                event,
733            } = event.opaque.as_ref().downcast_ref()?;
734
735            // Recovery from dirty shadow state.
736            if logical_tick == dirty_shadow_logical_tick {
737                *dirty_shadow_logical_tick = 0;
738            }
739
740            Self::apply_memory_changes(verse, event.clone());
741        }
742
743        Some(())
744    }
745
746    /// Apply memory changes to underlying compiler.
747    fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
748        if matches!(event, MemoryEvent::Sync(..)) {
749            vfs.reset_shadow();
750        }
751        match event {
752            MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
753                for path in event.removes {
754                    let _ = vfs.unmap_shadow(&path);
755                }
756                for (path, snap) in event.inserts {
757                    let _ = vfs.map_shadow(&path, snap);
758                }
759            }
760        }
761    }
762}
763
764/// A project instance state.
765pub struct ProjectInsState<F: CompilerFeat, Ext> {
766    /// The project instance id.
767    pub id: ProjectInsId,
768    /// The extension
769    pub ext: Ext,
770    /// The underlying universe.
771    pub verse: CompilerUniverse<F>,
772    /// Specifies the current export target.
773    pub export_target: ExportTarget,
774    /// Whether to run in syntax-only mode.
775    pub syntax_only: bool,
776    /// The reason to compile.
777    pub reason: CompileSignal,
778    /// The compilation handle.
779    pub handler: Arc<dyn CompileHandler<F, Ext>>,
780    /// The file dependencies.
781    deps: EcoVec<ImmutPath>,
782
783    /// The latest compute graph (snapshot), derived lazily from
784    /// `latest_compilation` as needed.
785    pub cached_snapshot: Option<Arc<WorldComputeGraph<F>>>,
786    /// The latest compilation.
787    pub latest_compilation: OnceLock<CompiledArtifact<F>>,
788    /// The latest successly compiled document.
789    pub latest_success_doc: Option<TypstDocument>,
790
791    committed_revision: usize,
792}
793
794impl<F: CompilerFeat, Ext: 'static> ProjectInsState<F, Ext> {
795    /// Gets a snapshot of the project.
796    pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
797        match self.cached_snapshot.as_ref() {
798            Some(snap) if snap.world().revision() == self.verse.revision => snap.clone(),
799            _ => {
800                let snap = self.make_snapshot();
801                self.cached_snapshot = Some(snap.clone());
802                snap
803            }
804        }
805    }
806
807    /// Creates a new snapshot of the project derived from `latest_compilation`.
808    fn make_snapshot(&self) -> Arc<WorldComputeGraph<F>> {
809        let world = self.verse.snapshot();
810        let snap = CompileSnapshot {
811            id: self.id.clone(),
812            world,
813            signal: self.reason,
814            success_doc: self.latest_success_doc.clone(),
815        };
816        WorldComputeGraph::new(snap)
817    }
818
819    /// Compiles the document once if there is any reason and the entry is
820    /// active. (this is used for experimenting typst.node compilations)
821    #[must_use]
822    pub fn may_compile2<'a>(
823        &mut self,
824        compute: impl FnOnce(&Arc<WorldComputeGraph<F>>) + 'a,
825    ) -> Option<impl FnOnce() -> Arc<WorldComputeGraph<F>> + 'a> {
826        if !self.reason.any() || self.verse.entry_state().is_inactive() {
827            return None;
828        }
829
830        let snap = self.snapshot();
831        self.reason = Default::default();
832        Some(move || {
833            compute(&snap);
834            snap
835        })
836    }
837
838    /// Compiles the document once if there is any reason and the entry is
839    /// active.
840    #[must_use]
841    pub fn may_compile(
842        &mut self,
843        handler: &Arc<dyn CompileHandler<F, Ext>>,
844    ) -> Option<impl FnOnce() -> CompiledArtifact<F> + 'static> {
845        if !self.reason.any() || self.verse.entry_state().is_inactive() {
846            return None;
847        }
848
849        let snap = self.snapshot();
850        self.reason = Default::default();
851
852        Some(Self::run_compile(
853            handler.clone(),
854            snap,
855            self.export_target,
856            self.syntax_only,
857        ))
858    }
859
860    /// Compile the document once.
861    fn run_compile(
862        h: Arc<dyn CompileHandler<F, Ext>>,
863        graph: Arc<WorldComputeGraph<F>>,
864        export_target: ExportTarget,
865        syntax_only: bool,
866    ) -> impl FnOnce() -> CompiledArtifact<F> {
867        let start = tinymist_std::time::Instant::now();
868
869        // todo unwrap main id
870        let id = graph.world().main_id().unwrap();
871        let revision = graph.world().revision().get();
872
873        h.status(revision, {
874            CompileReport {
875                id: graph.snap.id.clone(),
876                compiling_id: Some(id),
877                page_count: 0,
878                status: CompileStatusEnum::Compiling,
879            }
880        });
881
882        move || {
883            let compiled = if syntax_only {
884                let main = graph.snap.world.main();
885                let source_res = graph.world().source(main).at(Span::from_range(main, 0..0));
886                let syntax_res = source_res.and_then(|source| {
887                    let errors = source.root().errors();
888                    if errors.is_empty() {
889                        Ok(())
890                    } else {
891                        Err(errors.into_iter().map(|s| s.into()).collect())
892                    }
893                });
894                let diag = Arc::new(DiagnosticsTask::from_errors(syntax_res.err()));
895
896                CompiledArtifact {
897                    diag,
898                    graph,
899                    doc: None,
900                    deps: OnceLock::default(),
901                }
902            } else {
903                CompiledArtifact::from_graph(graph, matches!(export_target, ExportTarget::Html))
904            };
905
906            let res = CompileStatusResult {
907                diag: (compiled.warning_cnt() + compiled.error_cnt()) as u32,
908                elapsed: start.elapsed(),
909            };
910            let rep = CompileReport {
911                id: compiled.id().clone(),
912                compiling_id: Some(id),
913                page_count: compiled.doc.as_ref().map_or(0, |doc| doc.num_of_pages()),
914                status: match &compiled.doc {
915                    Some(..) => CompileStatusEnum::CompileSuccess(res),
916                    None if res.diag == 0 => CompileStatusEnum::CompileSuccess(res),
917                    None => CompileStatusEnum::CompileError(res),
918                },
919            };
920
921            // todo: we need to check revision for really concurrent compilation
922            log_compile_report(&rep);
923
924            if compiled
925                .diagnostics()
926                .any(|d| d.message == FILE_MISSING_ERROR_MSG)
927            {
928                return compiled;
929            }
930
931            h.status(revision, rep);
932            h.notify_compile(&compiled);
933            compiled
934        }
935    }
936
937    fn process_compile(&mut self, artifact: CompiledArtifact<F>) -> bool {
938        let world = &artifact.snap.world;
939        let compiled_revision = world.revision().get();
940        if self.committed_revision >= compiled_revision {
941            return false;
942        }
943
944        // Updates state.
945        let doc = artifact.doc.clone();
946        self.committed_revision = compiled_revision;
947        if doc.is_some() {
948            self.latest_success_doc = doc;
949        }
950        self.cached_snapshot = None; // invalidate; will be recomputed on demand
951
952        // Notifies the new file dependencies.
953        let mut deps = eco_vec![];
954        world.iter_dependencies(&mut |dep| {
955            if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
956                deps.push(x.into())
957            }
958        });
959
960        self.deps = deps.clone();
961
962        let mut world = world.clone();
963
964        let is_primary = self.id == ProjectInsId("primary".into());
965
966        // Trigger an evict task.
967        spawn_cpu(move || {
968            let evict_start = tinymist_std::time::Instant::now();
969            if is_primary {
970                comemo::evict(10);
971
972                // Since all the projects share the same cache, we need to evict the cache
973                // on the primary instance for all the projects.
974                world.evict_source_cache(30);
975            }
976            world.evict_vfs(60);
977            let elapsed = evict_start.elapsed();
978            log::debug!("ProjectCompiler: evict cache in {elapsed:?}");
979        });
980
981        true
982    }
983}
984
985fn log_compile_report(rep: &CompileReport) {
986    log::info!("{}", rep.message());
987}
988
989#[inline]
990fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
991    res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
992        .is_ok()
993}
994
995#[derive(Debug, Clone, Default)]
996struct ProjectDeps {
997    project_deps: rpds::RedBlackTreeMapSync<ProjectInsId, EcoVec<ImmutPath>>,
998}
999
1000impl NotifyDeps for ProjectDeps {
1001    fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath)) {
1002        for deps in self.project_deps.values().flat_map(|e| e.iter()) {
1003            f(deps);
1004        }
1005    }
1006}
1007
1008// todo: move me to tinymist-std
1009#[cfg(not(target_arch = "wasm32"))]
1010/// Spawns a CPU thread to run a computing-heavy task.
1011pub fn spawn_cpu<F>(func: F)
1012where
1013    F: FnOnce() + Send + 'static,
1014{
1015    rayon::spawn(func);
1016}
1017
1018#[cfg(target_arch = "wasm32")]
1019/// Spawns a CPU thread to run a computing-heavy task.
1020pub fn spawn_cpu<F>(func: F)
1021where
1022    F: FnOnce() + Send + 'static,
1023{
1024    func();
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030
1031    use std::path::PathBuf;
1032
1033    use tinymist_world::{
1034        mock::{MockCompilerFeat, MockWorkspaceWorldExt},
1035        vfs::{
1036            FileChangeSet, FileSnapshot, FilesystemEvent,
1037            mock::{MockChange, MockWorkspace},
1038        },
1039    };
1040    use tokio::sync::mpsc;
1041    use typst::{
1042        diag::{FileError, FileResult},
1043        foundations::Bytes,
1044    };
1045
1046    use crate::mock::{MockProjectBuilderExt, MockProjectChangeExt, MockProjectCompiler};
1047
1048    const MAIN: &str = "main.typ";
1049    const DEP: &str = "dep.typ";
1050    const RENAMED_DEP: &str = "renamed.typ";
1051    const UNRELATED: &str = "notes.typ";
1052
1053    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1054    enum MatrixOperation {
1055        InitialSync,
1056        FollowUpNonSyncUpdate,
1057        CreateDependency,
1058        EditEntry,
1059        EditDependency,
1060        CreateUnrelated,
1061        RemoveDependency,
1062        ReadErrorDependency,
1063        EmptyDependency,
1064        EmptyUnrelated,
1065        RenameUpdatedReferences,
1066        RenameStaleReferences,
1067        DeleteThenRecreate,
1068        FailedReadThenRecovery,
1069        RenameBatch,
1070        MultiFileUnrelatedBatch,
1071        UpstreamInvalidation,
1072        UnrelatedChurn,
1073        EmptyChangeset,
1074    }
1075
1076    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1077    enum EventVariant {
1078        Update,
1079        UpstreamUpdate,
1080    }
1081
1082    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1083    enum SyncMode {
1084        Sync,
1085        NonSync,
1086        NotApplicable,
1087    }
1088
1089    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1090    enum InsertPayload {
1091        NonEmptyContent,
1092        EmptyContent,
1093        ReadErrorSnapshot,
1094        NoInserts,
1095    }
1096
1097    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1098    enum RemovePayload {
1099        NoRemoves,
1100        OneRemovedPath,
1101        MultipleRemovedPaths,
1102    }
1103
1104    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1105    enum PathRelation {
1106        EntryFile,
1107        ImportedDependency,
1108        PreviouslyDependedPath,
1109        NewlyCreatedDependency,
1110        NewlyReferencedDependency,
1111        UnrelatedFile,
1112    }
1113
1114    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1115    enum BatchShape {
1116        InsertOnly,
1117        RemoveOnly,
1118        RemovePlusInsert,
1119        MultiFileBatch,
1120        EmptyChangeset,
1121        RemoveOnlyThenInsertOnly,
1122    }
1123
1124    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1125    enum SequenceShape {
1126        InitialSync,
1127        OneStepEdit,
1128        CreateAfterMissingImport,
1129        OneStepRemove,
1130        RenameOldPlusNew,
1131        FailedRead,
1132        FailedReadThenRecovery,
1133        TransientEmptyWrite,
1134        DeleteThenRecreate,
1135        DelayedMemoryThenFilesystem,
1136        EmptyChangeset,
1137    }
1138
1139    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1140    enum ExpectedOutcome {
1141        IgnoredFirstSync,
1142        FsReasonRefreshesDependency,
1143        RecoversNewDependency,
1144        RefreshesEntrySource,
1145        RefreshesDependencySource,
1146        KeepsUnrelatedCreateHarmless,
1147        ReportsRetiredDependencyUnavailable,
1148        SurfacesReadErrorDiagnostics,
1149        UsesEmptyDependencySnapshot,
1150        KeepsEmptyUnrelatedHarmless,
1151        FollowsRenamedPath,
1152        ReportsOldImportUnavailable,
1153        ReportsThenRecoversRecreatedSource,
1154        ClearsDiagnosticsAfterRecovery,
1155        RenameBatchFollowsRenamedPath,
1156        MultiFileUnrelatedBatchHarmless,
1157        AppliesDelayedMemoryBeforeFilesystem,
1158        KeepsUnrelatedChurnHarmless,
1159        ExplicitNoContentOutcome,
1160    }
1161
1162    #[derive(Debug, Clone, Copy)]
1163    struct MatrixRow {
1164        operation: MatrixOperation,
1165        event_variant: EventVariant,
1166        sync_mode: SyncMode,
1167        insert_payload: InsertPayload,
1168        remove_payload: RemovePayload,
1169        path_relations: &'static [PathRelation],
1170        batch_shape: BatchShape,
1171        sequence_shape: SequenceShape,
1172        expected: ExpectedOutcome,
1173    }
1174
1175    impl MatrixRow {
1176        fn sync_bool(self) -> bool {
1177            match self.sync_mode {
1178                SyncMode::Sync => true,
1179                SyncMode::NonSync => false,
1180                SyncMode::NotApplicable => {
1181                    panic!(
1182                        "matrix row {:?} does not carry an update sync flag",
1183                        self.operation
1184                    )
1185                }
1186            }
1187        }
1188
1189        fn apply_update(self, harness: &mut ProjectCompilerHarness, change: &MockChange) {
1190            assert_eq!(self.event_variant, EventVariant::Update);
1191            harness.apply_update(change, self.sync_bool());
1192        }
1193    }
1194
1195    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1196    enum OmittedEventCombination {
1197        SyncFlagOnUpstreamUpdate,
1198        EntryFileReadErrorAfterDirectClientInput,
1199        BackendSpecificNotifyRenameQuirk,
1200    }
1201
1202    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1203    enum OmissionReason {
1204        Unreachable,
1205        Redundant,
1206        Deferred,
1207    }
1208
1209    #[derive(Debug)]
1210    struct OmittedCombination {
1211        combination: OmittedEventCombination,
1212        reason: OmissionReason,
1213    }
1214
1215    const PROJECT_COMPILER_FS_EVENT_MATRIX: &[MatrixRow] = &[
1216        MatrixRow {
1217            operation: MatrixOperation::InitialSync,
1218            event_variant: EventVariant::Update,
1219            sync_mode: SyncMode::Sync,
1220            insert_payload: InsertPayload::NonEmptyContent,
1221            remove_payload: RemovePayload::NoRemoves,
1222            path_relations: &[PathRelation::EntryFile, PathRelation::ImportedDependency],
1223            batch_shape: BatchShape::MultiFileBatch,
1224            sequence_shape: SequenceShape::InitialSync,
1225            expected: ExpectedOutcome::IgnoredFirstSync,
1226        },
1227        MatrixRow {
1228            operation: MatrixOperation::FollowUpNonSyncUpdate,
1229            event_variant: EventVariant::Update,
1230            sync_mode: SyncMode::NonSync,
1231            insert_payload: InsertPayload::NonEmptyContent,
1232            remove_payload: RemovePayload::NoRemoves,
1233            path_relations: &[PathRelation::ImportedDependency],
1234            batch_shape: BatchShape::InsertOnly,
1235            sequence_shape: SequenceShape::OneStepEdit,
1236            expected: ExpectedOutcome::FsReasonRefreshesDependency,
1237        },
1238        MatrixRow {
1239            operation: MatrixOperation::CreateDependency,
1240            event_variant: EventVariant::Update,
1241            sync_mode: SyncMode::NonSync,
1242            insert_payload: InsertPayload::NonEmptyContent,
1243            remove_payload: RemovePayload::NoRemoves,
1244            path_relations: &[PathRelation::NewlyCreatedDependency],
1245            batch_shape: BatchShape::InsertOnly,
1246            sequence_shape: SequenceShape::CreateAfterMissingImport,
1247            expected: ExpectedOutcome::RecoversNewDependency,
1248        },
1249        MatrixRow {
1250            operation: MatrixOperation::EditEntry,
1251            event_variant: EventVariant::Update,
1252            sync_mode: SyncMode::NonSync,
1253            insert_payload: InsertPayload::NonEmptyContent,
1254            remove_payload: RemovePayload::NoRemoves,
1255            path_relations: &[PathRelation::EntryFile],
1256            batch_shape: BatchShape::InsertOnly,
1257            sequence_shape: SequenceShape::OneStepEdit,
1258            expected: ExpectedOutcome::RefreshesEntrySource,
1259        },
1260        MatrixRow {
1261            operation: MatrixOperation::EditDependency,
1262            event_variant: EventVariant::Update,
1263            sync_mode: SyncMode::NonSync,
1264            insert_payload: InsertPayload::NonEmptyContent,
1265            remove_payload: RemovePayload::NoRemoves,
1266            path_relations: &[PathRelation::ImportedDependency],
1267            batch_shape: BatchShape::InsertOnly,
1268            sequence_shape: SequenceShape::OneStepEdit,
1269            expected: ExpectedOutcome::RefreshesDependencySource,
1270        },
1271        MatrixRow {
1272            operation: MatrixOperation::CreateUnrelated,
1273            event_variant: EventVariant::Update,
1274            sync_mode: SyncMode::NonSync,
1275            insert_payload: InsertPayload::NonEmptyContent,
1276            remove_payload: RemovePayload::NoRemoves,
1277            path_relations: &[PathRelation::UnrelatedFile],
1278            batch_shape: BatchShape::InsertOnly,
1279            sequence_shape: SequenceShape::OneStepEdit,
1280            expected: ExpectedOutcome::KeepsUnrelatedCreateHarmless,
1281        },
1282        MatrixRow {
1283            operation: MatrixOperation::RemoveDependency,
1284            event_variant: EventVariant::Update,
1285            sync_mode: SyncMode::NonSync,
1286            insert_payload: InsertPayload::NoInserts,
1287            remove_payload: RemovePayload::OneRemovedPath,
1288            path_relations: &[PathRelation::PreviouslyDependedPath],
1289            batch_shape: BatchShape::RemoveOnly,
1290            sequence_shape: SequenceShape::OneStepRemove,
1291            expected: ExpectedOutcome::ReportsRetiredDependencyUnavailable,
1292        },
1293        MatrixRow {
1294            operation: MatrixOperation::ReadErrorDependency,
1295            event_variant: EventVariant::Update,
1296            sync_mode: SyncMode::NonSync,
1297            insert_payload: InsertPayload::ReadErrorSnapshot,
1298            remove_payload: RemovePayload::NoRemoves,
1299            path_relations: &[PathRelation::ImportedDependency],
1300            batch_shape: BatchShape::InsertOnly,
1301            sequence_shape: SequenceShape::FailedRead,
1302            expected: ExpectedOutcome::SurfacesReadErrorDiagnostics,
1303        },
1304        MatrixRow {
1305            operation: MatrixOperation::EmptyDependency,
1306            event_variant: EventVariant::Update,
1307            sync_mode: SyncMode::NonSync,
1308            insert_payload: InsertPayload::EmptyContent,
1309            remove_payload: RemovePayload::NoRemoves,
1310            path_relations: &[PathRelation::ImportedDependency],
1311            batch_shape: BatchShape::InsertOnly,
1312            sequence_shape: SequenceShape::TransientEmptyWrite,
1313            expected: ExpectedOutcome::UsesEmptyDependencySnapshot,
1314        },
1315        MatrixRow {
1316            operation: MatrixOperation::EmptyUnrelated,
1317            event_variant: EventVariant::Update,
1318            sync_mode: SyncMode::NonSync,
1319            insert_payload: InsertPayload::EmptyContent,
1320            remove_payload: RemovePayload::NoRemoves,
1321            path_relations: &[PathRelation::UnrelatedFile],
1322            batch_shape: BatchShape::InsertOnly,
1323            sequence_shape: SequenceShape::TransientEmptyWrite,
1324            expected: ExpectedOutcome::KeepsEmptyUnrelatedHarmless,
1325        },
1326        MatrixRow {
1327            operation: MatrixOperation::RenameUpdatedReferences,
1328            event_variant: EventVariant::Update,
1329            sync_mode: SyncMode::NonSync,
1330            insert_payload: InsertPayload::NonEmptyContent,
1331            remove_payload: RemovePayload::OneRemovedPath,
1332            path_relations: &[
1333                PathRelation::PreviouslyDependedPath,
1334                PathRelation::NewlyReferencedDependency,
1335            ],
1336            batch_shape: BatchShape::RemovePlusInsert,
1337            sequence_shape: SequenceShape::RenameOldPlusNew,
1338            expected: ExpectedOutcome::FollowsRenamedPath,
1339        },
1340        MatrixRow {
1341            operation: MatrixOperation::RenameStaleReferences,
1342            event_variant: EventVariant::Update,
1343            sync_mode: SyncMode::NonSync,
1344            insert_payload: InsertPayload::NonEmptyContent,
1345            remove_payload: RemovePayload::OneRemovedPath,
1346            path_relations: &[PathRelation::PreviouslyDependedPath],
1347            batch_shape: BatchShape::RemovePlusInsert,
1348            sequence_shape: SequenceShape::RenameOldPlusNew,
1349            expected: ExpectedOutcome::ReportsOldImportUnavailable,
1350        },
1351        MatrixRow {
1352            operation: MatrixOperation::DeleteThenRecreate,
1353            event_variant: EventVariant::Update,
1354            sync_mode: SyncMode::NonSync,
1355            insert_payload: InsertPayload::NonEmptyContent,
1356            remove_payload: RemovePayload::OneRemovedPath,
1357            path_relations: &[PathRelation::PreviouslyDependedPath],
1358            batch_shape: BatchShape::RemoveOnlyThenInsertOnly,
1359            sequence_shape: SequenceShape::DeleteThenRecreate,
1360            expected: ExpectedOutcome::ReportsThenRecoversRecreatedSource,
1361        },
1362        MatrixRow {
1363            operation: MatrixOperation::FailedReadThenRecovery,
1364            event_variant: EventVariant::Update,
1365            sync_mode: SyncMode::NonSync,
1366            insert_payload: InsertPayload::NonEmptyContent,
1367            remove_payload: RemovePayload::NoRemoves,
1368            path_relations: &[PathRelation::ImportedDependency],
1369            batch_shape: BatchShape::InsertOnly,
1370            sequence_shape: SequenceShape::FailedReadThenRecovery,
1371            expected: ExpectedOutcome::ClearsDiagnosticsAfterRecovery,
1372        },
1373        MatrixRow {
1374            operation: MatrixOperation::RenameBatch,
1375            event_variant: EventVariant::Update,
1376            sync_mode: SyncMode::NonSync,
1377            insert_payload: InsertPayload::NonEmptyContent,
1378            remove_payload: RemovePayload::OneRemovedPath,
1379            path_relations: &[
1380                PathRelation::PreviouslyDependedPath,
1381                PathRelation::NewlyReferencedDependency,
1382            ],
1383            batch_shape: BatchShape::RemovePlusInsert,
1384            sequence_shape: SequenceShape::RenameOldPlusNew,
1385            expected: ExpectedOutcome::RenameBatchFollowsRenamedPath,
1386        },
1387        MatrixRow {
1388            operation: MatrixOperation::MultiFileUnrelatedBatch,
1389            event_variant: EventVariant::Update,
1390            sync_mode: SyncMode::NonSync,
1391            insert_payload: InsertPayload::NonEmptyContent,
1392            remove_payload: RemovePayload::MultipleRemovedPaths,
1393            path_relations: &[PathRelation::UnrelatedFile],
1394            batch_shape: BatchShape::MultiFileBatch,
1395            sequence_shape: SequenceShape::OneStepEdit,
1396            expected: ExpectedOutcome::MultiFileUnrelatedBatchHarmless,
1397        },
1398        MatrixRow {
1399            operation: MatrixOperation::UpstreamInvalidation,
1400            event_variant: EventVariant::UpstreamUpdate,
1401            sync_mode: SyncMode::NotApplicable,
1402            insert_payload: InsertPayload::NonEmptyContent,
1403            remove_payload: RemovePayload::NoRemoves,
1404            path_relations: &[PathRelation::EntryFile],
1405            batch_shape: BatchShape::InsertOnly,
1406            sequence_shape: SequenceShape::DelayedMemoryThenFilesystem,
1407            expected: ExpectedOutcome::AppliesDelayedMemoryBeforeFilesystem,
1408        },
1409        MatrixRow {
1410            operation: MatrixOperation::UnrelatedChurn,
1411            event_variant: EventVariant::Update,
1412            sync_mode: SyncMode::NonSync,
1413            insert_payload: InsertPayload::NonEmptyContent,
1414            remove_payload: RemovePayload::NoRemoves,
1415            path_relations: &[PathRelation::UnrelatedFile],
1416            batch_shape: BatchShape::InsertOnly,
1417            sequence_shape: SequenceShape::OneStepEdit,
1418            expected: ExpectedOutcome::KeepsUnrelatedChurnHarmless,
1419        },
1420        MatrixRow {
1421            operation: MatrixOperation::EmptyChangeset,
1422            event_variant: EventVariant::Update,
1423            sync_mode: SyncMode::NonSync,
1424            insert_payload: InsertPayload::NoInserts,
1425            remove_payload: RemovePayload::NoRemoves,
1426            path_relations: &[PathRelation::UnrelatedFile],
1427            batch_shape: BatchShape::EmptyChangeset,
1428            sequence_shape: SequenceShape::EmptyChangeset,
1429            expected: ExpectedOutcome::ExplicitNoContentOutcome,
1430        },
1431    ];
1432
1433    const OMITTED_PROJECT_COMPILER_FS_EVENT_COMBINATIONS: &[OmittedCombination] = &[
1434        OmittedCombination {
1435            combination: OmittedEventCombination::SyncFlagOnUpstreamUpdate,
1436            reason: OmissionReason::Unreachable,
1437        },
1438        OmittedCombination {
1439            combination: OmittedEventCombination::EntryFileReadErrorAfterDirectClientInput,
1440            reason: OmissionReason::Redundant,
1441        },
1442        OmittedCombination {
1443            combination: OmittedEventCombination::BackendSpecificNotifyRenameQuirk,
1444            reason: OmissionReason::Deferred,
1445        },
1446    ];
1447
1448    struct ProjectCompilerHarness {
1449        workspace: MockWorkspace,
1450        compiler: MockProjectCompiler<()>,
1451        notify_rx: mpsc::UnboundedReceiver<NotifyMessage>,
1452    }
1453
1454    impl ProjectCompilerHarness {
1455        fn new(files: &[(&str, &str)]) -> Self {
1456            Self::with_opts(
1457                files,
1458                CompileServerOpts::<MockCompilerFeat, ()> {
1459                    syntax_only: false,
1460                    ..Default::default()
1461                },
1462            )
1463        }
1464
1465        fn ignoring_first_sync(files: &[(&str, &str)]) -> Self {
1466            Self::with_opts(
1467                files,
1468                CompileServerOpts::<MockCompilerFeat, ()> {
1469                    ignore_first_sync: true,
1470                    syntax_only: false,
1471                    ..Default::default()
1472                },
1473            )
1474        }
1475
1476        fn with_opts(
1477            files: &[(&str, &str)],
1478            opts: CompileServerOpts<MockCompilerFeat, ()>,
1479        ) -> Self {
1480            let mut builder = MockWorkspace::default_builder();
1481            for (path, source) in files {
1482                builder = builder.file(path, source.to_string());
1483            }
1484
1485            let workspace = builder.build();
1486            let (compiler, notify_rx) = workspace
1487                .world(MAIN)
1488                .project_compiler_with_opts::<()>(opts)
1489                .unwrap();
1490
1491            Self {
1492                workspace,
1493                compiler,
1494                notify_rx,
1495            }
1496        }
1497
1498        fn compile_primary(&mut self) -> CompiledArtifact<MockCompilerFeat> {
1499            self.compiler
1500                .process(Interrupt::Compile(ProjectInsId::PRIMARY));
1501            self.compile_pending()
1502        }
1503
1504        fn compile_pending(&mut self) -> CompiledArtifact<MockCompilerFeat> {
1505            assert!(
1506                self.compiler.primary.reason.any(),
1507                "expected a pending compile reason"
1508            );
1509
1510            let handler = self.compiler.handler.clone();
1511            let compile = self
1512                .compiler
1513                .primary
1514                .may_compile(&handler)
1515                .expect("expected the primary project to compile");
1516            let artifact = compile();
1517            self.compiler.process(Interrupt::Compiled(artifact.clone()));
1518            artifact
1519        }
1520
1521        fn apply_update(&mut self, change: &MockChange, is_sync: bool) {
1522            change.apply_as_fs_to_project(&mut self.compiler, is_sync);
1523        }
1524
1525        fn apply_upstream_update(
1526            &mut self,
1527            changeset: FileChangeSet,
1528            upstream_event: Option<UpstreamUpdateEvent>,
1529        ) {
1530            self.compiler
1531                .process(Interrupt::Fs(FilesystemEvent::UpstreamUpdate {
1532                    changeset,
1533                    upstream_event,
1534                }));
1535        }
1536
1537        fn take_upstream_update(&mut self) -> UpstreamUpdateEvent {
1538            loop {
1539                let message = self
1540                    .notify_rx
1541                    .try_recv()
1542                    .expect("expected an upstream update notification");
1543                match message {
1544                    NotifyMessage::UpstreamUpdate(event) => return event,
1545                    NotifyMessage::SyncDependency(..) | NotifyMessage::Settle => {}
1546                }
1547            }
1548        }
1549
1550        fn latest_sync_dependencies(&mut self) -> Vec<PathBuf> {
1551            self.optional_sync_dependencies()
1552                .expect("expected SyncDependency notification")
1553        }
1554
1555        fn optional_sync_dependencies(&mut self) -> Option<Vec<PathBuf>> {
1556            let mut latest = None;
1557            while let Ok(message) = self.notify_rx.try_recv() {
1558                if let NotifyMessage::SyncDependency(deps) = message {
1559                    let mut paths = Vec::new();
1560                    deps.dependencies(&mut |path| paths.push(path.as_ref().to_path_buf()));
1561                    latest = Some(paths);
1562                }
1563            }
1564
1565            latest
1566        }
1567
1568        fn dependency_paths_after_compile(&mut self) -> Vec<PathBuf> {
1569            self.latest_sync_dependencies()
1570        }
1571
1572        fn dependency_paths_after_harmless_compile(
1573            &mut self,
1574            previous: &[PathBuf],
1575        ) -> Vec<PathBuf> {
1576            self.optional_sync_dependencies()
1577                .unwrap_or_else(|| previous.to_vec())
1578        }
1579    }
1580
1581    fn default_files() -> Vec<(&'static str, &'static str)> {
1582        vec![
1583            (MAIN, "#import \"dep.typ\": value\n#value"),
1584            (DEP, "#let value = [before]"),
1585            (UNRELATED, "#let note = [unchanged]"),
1586        ]
1587    }
1588
1589    fn source_snapshot(source: &str) -> FileSnapshot {
1590        FileResult::Ok(Bytes::from_string(source.to_owned())).into()
1591    }
1592
1593    fn read_error_snapshot(path: PathBuf) -> FileSnapshot {
1594        FileResult::Err(FileError::NotFound(path)).into()
1595    }
1596
1597    fn insert_source_change(workspace: &MockWorkspace, path: &str, source: &str) -> MockChange {
1598        MockChange::new(FileChangeSet::new_inserts(vec![(
1599            workspace.immut_path(path),
1600            source_snapshot(source),
1601        )]))
1602    }
1603
1604    fn read_error_change(workspace: &MockWorkspace, path: &str) -> MockChange {
1605        MockChange::new(FileChangeSet::new_inserts(vec![(
1606            workspace.immut_path(path),
1607            read_error_snapshot(workspace.path(path)),
1608        )]))
1609    }
1610
1611    fn remove_change(workspace: &MockWorkspace, path: &str) -> MockChange {
1612        MockChange::new(FileChangeSet::new_removes(vec![workspace.immut_path(path)]))
1613    }
1614
1615    fn empty_change() -> MockChange {
1616        MockChange::new(FileChangeSet::default())
1617    }
1618
1619    fn combine_changes(changes: &[MockChange]) -> MockChange {
1620        let mut changeset = FileChangeSet::default();
1621        for change in changes {
1622            changeset.removes.extend(change.changeset().removes.clone());
1623            changeset.inserts.extend(change.changeset().inserts.clone());
1624        }
1625
1626        MockChange::new(changeset)
1627    }
1628
1629    fn source_text(
1630        artifact: &CompiledArtifact<MockCompilerFeat>,
1631        workspace: &MockWorkspace,
1632        path: &str,
1633    ) -> String {
1634        artifact
1635            .graph
1636            .snap
1637            .world
1638            .source_by_path(&workspace.path(path))
1639            .unwrap()
1640            .text()
1641            .to_owned()
1642    }
1643
1644    fn source_is_unavailable(
1645        artifact: &CompiledArtifact<MockCompilerFeat>,
1646        workspace: &MockWorkspace,
1647        path: &str,
1648    ) -> bool {
1649        artifact
1650            .graph
1651            .snap
1652            .world
1653            .source_by_path(&workspace.path(path))
1654            .is_err()
1655    }
1656
1657    fn assert_fs_reason(compiler: &MockProjectCompiler<()>) {
1658        assert!(
1659            compiler.primary.reason.by_fs_events,
1660            "expected filesystem compile reason"
1661        );
1662    }
1663
1664    fn assert_mem_reason(compiler: &MockProjectCompiler<()>) {
1665        assert!(
1666            compiler.primary.reason.by_mem_events,
1667            "expected memory compile reason"
1668        );
1669    }
1670
1671    fn assert_deps_contain(workspace: &MockWorkspace, deps: &[PathBuf], path: &str) {
1672        assert!(
1673            deps.contains(&workspace.path(path)),
1674            "expected dependencies to contain {path:?}; got {deps:?}"
1675        );
1676    }
1677
1678    fn assert_deps_do_not_contain(workspace: &MockWorkspace, deps: &[PathBuf], path: &str) {
1679        assert!(
1680            !deps.contains(&workspace.path(path)),
1681            "expected dependencies not to contain {path:?}; got {deps:?}"
1682        );
1683    }
1684
1685    fn assert_matrix_contains<T: std::fmt::Debug>(
1686        missing: T,
1687        predicate: impl Fn(&MatrixRow) -> bool,
1688    ) {
1689        assert!(
1690            PROJECT_COMPILER_FS_EVENT_MATRIX.iter().any(predicate),
1691            "project compiler filesystem event matrix missing {missing:?}"
1692        );
1693    }
1694
1695    #[expect(
1696        clippy::too_many_arguments,
1697        reason = "project compiler matrix rows are clearer when each dimension is asserted explicitly"
1698    )]
1699    fn assert_row_shape(
1700        row: MatrixRow,
1701        event_variant: EventVariant,
1702        sync_mode: SyncMode,
1703        insert_payload: InsertPayload,
1704        remove_payload: RemovePayload,
1705        path_relations: &[PathRelation],
1706        batch_shape: BatchShape,
1707        sequence_shape: SequenceShape,
1708        expected: ExpectedOutcome,
1709    ) {
1710        assert_eq!(row.event_variant, event_variant);
1711        assert_eq!(row.sync_mode, sync_mode);
1712        assert_eq!(row.insert_payload, insert_payload);
1713        assert_eq!(row.remove_payload, remove_payload);
1714        assert_eq!(row.path_relations, path_relations);
1715        assert_eq!(row.batch_shape, batch_shape);
1716        assert_eq!(row.sequence_shape, sequence_shape);
1717        assert_eq!(row.expected, expected);
1718    }
1719
1720    fn clean_default_harness_with_deps() -> (ProjectCompilerHarness, Vec<PathBuf>) {
1721        let files = default_files();
1722        let mut harness = ProjectCompilerHarness::new(&files);
1723        let initial = harness.compile_primary();
1724        assert_eq!(initial.error_cnt(), 0);
1725        let deps = harness.latest_sync_dependencies();
1726        (harness, deps)
1727    }
1728
1729    fn clean_default_harness() -> ProjectCompilerHarness {
1730        clean_default_harness_with_deps().0
1731    }
1732
1733    fn run_matrix_row(row: MatrixRow) {
1734        match row.operation {
1735            MatrixOperation::InitialSync => assert_initial_sync(row),
1736            MatrixOperation::FollowUpNonSyncUpdate => assert_follow_up_non_sync_update(row),
1737            MatrixOperation::CreateDependency => assert_create_dependency(row),
1738            MatrixOperation::EditEntry => assert_edit_entry(row),
1739            MatrixOperation::EditDependency => assert_edit_dependency(row),
1740            MatrixOperation::CreateUnrelated => assert_create_unrelated(row),
1741            MatrixOperation::RemoveDependency => assert_remove_dependency(row),
1742            MatrixOperation::ReadErrorDependency => assert_read_error_dependency(row),
1743            MatrixOperation::EmptyDependency => assert_empty_dependency(row),
1744            MatrixOperation::EmptyUnrelated => assert_empty_unrelated(row),
1745            MatrixOperation::RenameUpdatedReferences => assert_rename_updated_references(row),
1746            MatrixOperation::RenameStaleReferences => assert_rename_stale_references(row),
1747            MatrixOperation::DeleteThenRecreate => assert_delete_then_recreate(row),
1748            MatrixOperation::FailedReadThenRecovery => assert_failed_read_then_recovery(row),
1749            MatrixOperation::RenameBatch => assert_rename_batch(row),
1750            MatrixOperation::MultiFileUnrelatedBatch => assert_multi_file_unrelated_batch(row),
1751            MatrixOperation::UpstreamInvalidation => assert_upstream_invalidation(row),
1752            MatrixOperation::UnrelatedChurn => assert_unrelated_churn(row),
1753            MatrixOperation::EmptyChangeset => assert_empty_changeset(row),
1754        }
1755    }
1756
1757    #[test]
1758    fn project_compiler_fs_event_matrix_is_explicit() {
1759        for row in PROJECT_COMPILER_FS_EVENT_MATRIX {
1760            assert!(!row.path_relations.is_empty());
1761        }
1762
1763        for operation in [
1764            MatrixOperation::InitialSync,
1765            MatrixOperation::FollowUpNonSyncUpdate,
1766            MatrixOperation::CreateDependency,
1767            MatrixOperation::EditEntry,
1768            MatrixOperation::EditDependency,
1769            MatrixOperation::CreateUnrelated,
1770            MatrixOperation::RemoveDependency,
1771            MatrixOperation::ReadErrorDependency,
1772            MatrixOperation::EmptyDependency,
1773            MatrixOperation::EmptyUnrelated,
1774            MatrixOperation::RenameUpdatedReferences,
1775            MatrixOperation::RenameStaleReferences,
1776            MatrixOperation::DeleteThenRecreate,
1777            MatrixOperation::FailedReadThenRecovery,
1778            MatrixOperation::RenameBatch,
1779            MatrixOperation::MultiFileUnrelatedBatch,
1780            MatrixOperation::UpstreamInvalidation,
1781            MatrixOperation::UnrelatedChurn,
1782            MatrixOperation::EmptyChangeset,
1783        ] {
1784            assert_matrix_contains(operation, |row| row.operation == operation);
1785        }
1786        for variant in [EventVariant::Update, EventVariant::UpstreamUpdate] {
1787            assert_matrix_contains(variant, |row| row.event_variant == variant);
1788        }
1789        for sync_mode in [SyncMode::Sync, SyncMode::NonSync, SyncMode::NotApplicable] {
1790            assert_matrix_contains(sync_mode, |row| row.sync_mode == sync_mode);
1791        }
1792        for payload in [
1793            InsertPayload::NonEmptyContent,
1794            InsertPayload::EmptyContent,
1795            InsertPayload::ReadErrorSnapshot,
1796            InsertPayload::NoInserts,
1797        ] {
1798            assert_matrix_contains(payload, |row| row.insert_payload == payload);
1799        }
1800        for payload in [
1801            RemovePayload::NoRemoves,
1802            RemovePayload::OneRemovedPath,
1803            RemovePayload::MultipleRemovedPaths,
1804        ] {
1805            assert_matrix_contains(payload, |row| row.remove_payload == payload);
1806        }
1807        for relation in [
1808            PathRelation::EntryFile,
1809            PathRelation::ImportedDependency,
1810            PathRelation::PreviouslyDependedPath,
1811            PathRelation::NewlyCreatedDependency,
1812            PathRelation::NewlyReferencedDependency,
1813            PathRelation::UnrelatedFile,
1814        ] {
1815            assert_matrix_contains(relation, |row| row.path_relations.contains(&relation));
1816        }
1817        for batch in [
1818            BatchShape::InsertOnly,
1819            BatchShape::RemoveOnly,
1820            BatchShape::RemovePlusInsert,
1821            BatchShape::MultiFileBatch,
1822            BatchShape::EmptyChangeset,
1823            BatchShape::RemoveOnlyThenInsertOnly,
1824        ] {
1825            assert_matrix_contains(batch, |row| row.batch_shape == batch);
1826        }
1827        for sequence in [
1828            SequenceShape::InitialSync,
1829            SequenceShape::OneStepEdit,
1830            SequenceShape::CreateAfterMissingImport,
1831            SequenceShape::OneStepRemove,
1832            SequenceShape::RenameOldPlusNew,
1833            SequenceShape::FailedRead,
1834            SequenceShape::FailedReadThenRecovery,
1835            SequenceShape::TransientEmptyWrite,
1836            SequenceShape::DeleteThenRecreate,
1837            SequenceShape::DelayedMemoryThenFilesystem,
1838            SequenceShape::EmptyChangeset,
1839        ] {
1840            assert_matrix_contains(sequence, |row| row.sequence_shape == sequence);
1841        }
1842
1843        for omitted in OMITTED_PROJECT_COMPILER_FS_EVENT_COMBINATIONS {
1844            assert!(matches!(
1845                omitted.reason,
1846                OmissionReason::Unreachable | OmissionReason::Redundant | OmissionReason::Deferred
1847            ));
1848            assert!(matches!(
1849                omitted.combination,
1850                OmittedEventCombination::SyncFlagOnUpstreamUpdate
1851                    | OmittedEventCombination::EntryFileReadErrorAfterDirectClientInput
1852                    | OmittedEventCombination::BackendSpecificNotifyRenameQuirk
1853            ));
1854        }
1855    }
1856
1857    #[test]
1858    fn project_compiler_fs_event_matrix_rows_execute_expected_outcomes() {
1859        for row in PROJECT_COMPILER_FS_EVENT_MATRIX {
1860            run_matrix_row(*row);
1861        }
1862    }
1863
1864    fn assert_initial_sync(row: MatrixRow) {
1865        assert_row_shape(
1866            row,
1867            EventVariant::Update,
1868            SyncMode::Sync,
1869            InsertPayload::NonEmptyContent,
1870            RemovePayload::NoRemoves,
1871            &[PathRelation::EntryFile, PathRelation::ImportedDependency],
1872            BatchShape::MultiFileBatch,
1873            SequenceShape::InitialSync,
1874            ExpectedOutcome::IgnoredFirstSync,
1875        );
1876
1877        let files = default_files();
1878        let mut harness = ProjectCompilerHarness::ignoring_first_sync(&files);
1879        let initial = harness.compile_primary();
1880        assert_eq!(initial.error_cnt(), 0);
1881        harness.latest_sync_dependencies();
1882
1883        let sync = MockChange::new(harness.workspace.sync_changeset());
1884        row.apply_update(&mut harness, &sync);
1885        assert!(
1886            !harness.compiler.primary.reason.any(),
1887            "initial sync should not create a compile reason when ignored"
1888        );
1889    }
1890
1891    fn assert_follow_up_non_sync_update(row: MatrixRow) {
1892        assert_row_shape(
1893            row,
1894            EventVariant::Update,
1895            SyncMode::NonSync,
1896            InsertPayload::NonEmptyContent,
1897            RemovePayload::NoRemoves,
1898            &[PathRelation::ImportedDependency],
1899            BatchShape::InsertOnly,
1900            SequenceShape::OneStepEdit,
1901            ExpectedOutcome::FsReasonRefreshesDependency,
1902        );
1903
1904        let files = default_files();
1905        let mut harness = ProjectCompilerHarness::ignoring_first_sync(&files);
1906        let initial = harness.compile_primary();
1907        assert_eq!(initial.error_cnt(), 0);
1908        harness.latest_sync_dependencies();
1909
1910        let sync = MockChange::new(harness.workspace.sync_changeset());
1911        harness.apply_update(&sync, true);
1912        assert!(!harness.compiler.primary.reason.any());
1913
1914        let follow_up = harness
1915            .workspace
1916            .update_source(DEP, "#let value = [after sync]");
1917        row.apply_update(&mut harness, &follow_up);
1918        assert_fs_reason(&harness.compiler);
1919
1920        let artifact = harness.compile_pending();
1921        assert_eq!(artifact.error_cnt(), 0);
1922        assert_eq!(
1923            source_text(&artifact, &harness.workspace, DEP),
1924            "#let value = [after sync]"
1925        );
1926        let deps = harness.dependency_paths_after_compile();
1927        assert_deps_contain(&harness.workspace, &deps, DEP);
1928    }
1929
1930    fn assert_create_dependency(row: MatrixRow) {
1931        assert_row_shape(
1932            row,
1933            EventVariant::Update,
1934            SyncMode::NonSync,
1935            InsertPayload::NonEmptyContent,
1936            RemovePayload::NoRemoves,
1937            &[PathRelation::NewlyCreatedDependency],
1938            BatchShape::InsertOnly,
1939            SequenceShape::CreateAfterMissingImport,
1940            ExpectedOutcome::RecoversNewDependency,
1941        );
1942
1943        let files = vec![
1944            (
1945                MAIN,
1946                "#import \"dep.typ\": value\n#import \"new.typ\": newer\n#value\n#newer",
1947            ),
1948            (DEP, "#let value = [before]"),
1949        ];
1950        let mut harness = ProjectCompilerHarness::new(&files);
1951        let initial = harness.compile_primary();
1952        assert!(initial.error_cnt() > 0);
1953        harness.latest_sync_dependencies();
1954
1955        let created_dependency = harness
1956            .workspace
1957            .create_source("new.typ", "#let newer = [new dependency]");
1958        row.apply_update(&mut harness, &created_dependency);
1959        assert_fs_reason(&harness.compiler);
1960        let artifact = harness.compile_pending();
1961        assert_eq!(artifact.error_cnt(), 0);
1962        assert_eq!(
1963            source_text(&artifact, &harness.workspace, "new.typ"),
1964            "#let newer = [new dependency]"
1965        );
1966        let deps = harness.dependency_paths_after_compile();
1967        assert_deps_contain(&harness.workspace, &deps, "new.typ");
1968    }
1969
1970    fn assert_edit_entry(row: MatrixRow) {
1971        assert_row_shape(
1972            row,
1973            EventVariant::Update,
1974            SyncMode::NonSync,
1975            InsertPayload::NonEmptyContent,
1976            RemovePayload::NoRemoves,
1977            &[PathRelation::EntryFile],
1978            BatchShape::InsertOnly,
1979            SequenceShape::OneStepEdit,
1980            ExpectedOutcome::RefreshesEntrySource,
1981        );
1982
1983        let mut harness = clean_default_harness();
1984        let entry_edit = harness.workspace.update_source(
1985            MAIN,
1986            "#import \"dep.typ\": value\n#let local = [entry changed]\n#value\n#local",
1987        );
1988        row.apply_update(&mut harness, &entry_edit);
1989        assert_fs_reason(&harness.compiler);
1990        let artifact = harness.compile_pending();
1991        assert_eq!(artifact.error_cnt(), 0);
1992        assert_eq!(
1993            source_text(&artifact, &harness.workspace, MAIN),
1994            "#import \"dep.typ\": value\n#let local = [entry changed]\n#value\n#local"
1995        );
1996    }
1997
1998    fn assert_edit_dependency(row: MatrixRow) {
1999        assert_row_shape(
2000            row,
2001            EventVariant::Update,
2002            SyncMode::NonSync,
2003            InsertPayload::NonEmptyContent,
2004            RemovePayload::NoRemoves,
2005            &[PathRelation::ImportedDependency],
2006            BatchShape::InsertOnly,
2007            SequenceShape::OneStepEdit,
2008            ExpectedOutcome::RefreshesDependencySource,
2009        );
2010
2011        let mut harness = clean_default_harness();
2012        let dependency_edit = harness
2013            .workspace
2014            .update_source(DEP, "#let value = [dependency changed]");
2015        row.apply_update(&mut harness, &dependency_edit);
2016        assert_fs_reason(&harness.compiler);
2017        let artifact = harness.compile_pending();
2018        assert_eq!(artifact.error_cnt(), 0);
2019        assert_eq!(
2020            source_text(&artifact, &harness.workspace, DEP),
2021            "#let value = [dependency changed]"
2022        );
2023    }
2024
2025    fn assert_create_unrelated(row: MatrixRow) {
2026        assert_row_shape(
2027            row,
2028            EventVariant::Update,
2029            SyncMode::NonSync,
2030            InsertPayload::NonEmptyContent,
2031            RemovePayload::NoRemoves,
2032            &[PathRelation::UnrelatedFile],
2033            BatchShape::InsertOnly,
2034            SequenceShape::OneStepEdit,
2035            ExpectedOutcome::KeepsUnrelatedCreateHarmless,
2036        );
2037
2038        let (mut harness, deps_before) = clean_default_harness_with_deps();
2039        let unrelated_create = harness
2040            .workspace
2041            .create_source("scratch.typ", "#let scratch = [unused]");
2042        row.apply_update(&mut harness, &unrelated_create);
2043        assert_fs_reason(&harness.compiler);
2044        let artifact = harness.compile_pending();
2045        assert_eq!(artifact.error_cnt(), 0);
2046        let deps_after = harness.dependency_paths_after_harmless_compile(&deps_before);
2047        assert_eq!(deps_after, deps_before);
2048        assert_deps_do_not_contain(&harness.workspace, &deps_after, "scratch.typ");
2049    }
2050
2051    fn assert_remove_dependency(row: MatrixRow) {
2052        assert_row_shape(
2053            row,
2054            EventVariant::Update,
2055            SyncMode::NonSync,
2056            InsertPayload::NoInserts,
2057            RemovePayload::OneRemovedPath,
2058            &[PathRelation::PreviouslyDependedPath],
2059            BatchShape::RemoveOnly,
2060            SequenceShape::OneStepRemove,
2061            ExpectedOutcome::ReportsRetiredDependencyUnavailable,
2062        );
2063
2064        let mut harness = clean_default_harness();
2065        let removed = harness.workspace.remove(DEP).unwrap();
2066        row.apply_update(&mut harness, &removed);
2067        assert_fs_reason(&harness.compiler);
2068        let artifact = harness.compile_pending();
2069        assert!(artifact.error_cnt() > 0);
2070        assert!(source_is_unavailable(&artifact, &harness.workspace, DEP));
2071    }
2072
2073    fn assert_read_error_dependency(row: MatrixRow) {
2074        assert_row_shape(
2075            row,
2076            EventVariant::Update,
2077            SyncMode::NonSync,
2078            InsertPayload::ReadErrorSnapshot,
2079            RemovePayload::NoRemoves,
2080            &[PathRelation::ImportedDependency],
2081            BatchShape::InsertOnly,
2082            SequenceShape::FailedRead,
2083            ExpectedOutcome::SurfacesReadErrorDiagnostics,
2084        );
2085
2086        let mut harness = clean_default_harness();
2087        let read_error = read_error_change(&harness.workspace, DEP);
2088        row.apply_update(&mut harness, &read_error);
2089        assert_fs_reason(&harness.compiler);
2090        let artifact = harness.compile_pending();
2091        assert!(artifact.error_cnt() > 0);
2092        assert!(source_is_unavailable(&artifact, &harness.workspace, DEP));
2093    }
2094
2095    fn assert_empty_dependency(row: MatrixRow) {
2096        assert_row_shape(
2097            row,
2098            EventVariant::Update,
2099            SyncMode::NonSync,
2100            InsertPayload::EmptyContent,
2101            RemovePayload::NoRemoves,
2102            &[PathRelation::ImportedDependency],
2103            BatchShape::InsertOnly,
2104            SequenceShape::TransientEmptyWrite,
2105            ExpectedOutcome::UsesEmptyDependencySnapshot,
2106        );
2107
2108        let mut harness = clean_default_harness();
2109        let empty_dependency = harness.workspace.update_source(DEP, "");
2110        row.apply_update(&mut harness, &empty_dependency);
2111        assert_fs_reason(&harness.compiler);
2112        let artifact = harness.compile_pending();
2113        assert!(artifact.error_cnt() > 0);
2114        assert_eq!(source_text(&artifact, &harness.workspace, DEP), "");
2115    }
2116
2117    fn assert_empty_unrelated(row: MatrixRow) {
2118        assert_row_shape(
2119            row,
2120            EventVariant::Update,
2121            SyncMode::NonSync,
2122            InsertPayload::EmptyContent,
2123            RemovePayload::NoRemoves,
2124            &[PathRelation::UnrelatedFile],
2125            BatchShape::InsertOnly,
2126            SequenceShape::TransientEmptyWrite,
2127            ExpectedOutcome::KeepsEmptyUnrelatedHarmless,
2128        );
2129
2130        let (mut harness, deps_before) = clean_default_harness_with_deps();
2131        let empty_unrelated = harness.workspace.update_source(UNRELATED, "");
2132        row.apply_update(&mut harness, &empty_unrelated);
2133        assert_fs_reason(&harness.compiler);
2134        let artifact = harness.compile_pending();
2135        assert_eq!(artifact.error_cnt(), 0);
2136        let deps_after = harness.dependency_paths_after_harmless_compile(&deps_before);
2137        assert_eq!(deps_after, deps_before);
2138        assert_deps_do_not_contain(&harness.workspace, &deps_after, UNRELATED);
2139    }
2140
2141    fn assert_rename_updated_references(row: MatrixRow) {
2142        assert_row_shape(
2143            row,
2144            EventVariant::Update,
2145            SyncMode::NonSync,
2146            InsertPayload::NonEmptyContent,
2147            RemovePayload::OneRemovedPath,
2148            &[
2149                PathRelation::PreviouslyDependedPath,
2150                PathRelation::NewlyReferencedDependency,
2151            ],
2152            BatchShape::RemovePlusInsert,
2153            SequenceShape::RenameOldPlusNew,
2154            ExpectedOutcome::FollowsRenamedPath,
2155        );
2156
2157        let mut harness = clean_default_harness();
2158        let rename = harness.workspace.rename(DEP, RENAMED_DEP).unwrap();
2159        row.apply_update(&mut harness, &rename);
2160        let entry_update = harness
2161            .workspace
2162            .update_source(MAIN, "#import \"renamed.typ\": value\n#value");
2163        harness.apply_update(&entry_update, false);
2164        assert_fs_reason(&harness.compiler);
2165        let artifact = harness.compile_pending();
2166        assert_eq!(artifact.error_cnt(), 0);
2167        assert!(source_is_unavailable(&artifact, &harness.workspace, DEP));
2168        assert_eq!(
2169            source_text(&artifact, &harness.workspace, RENAMED_DEP),
2170            "#let value = [before]"
2171        );
2172        let deps = harness.dependency_paths_after_compile();
2173        assert_deps_contain(&harness.workspace, &deps, RENAMED_DEP);
2174        assert_deps_do_not_contain(&harness.workspace, &deps, DEP);
2175    }
2176
2177    fn assert_rename_stale_references(row: MatrixRow) {
2178        assert_row_shape(
2179            row,
2180            EventVariant::Update,
2181            SyncMode::NonSync,
2182            InsertPayload::NonEmptyContent,
2183            RemovePayload::OneRemovedPath,
2184            &[PathRelation::PreviouslyDependedPath],
2185            BatchShape::RemovePlusInsert,
2186            SequenceShape::RenameOldPlusNew,
2187            ExpectedOutcome::ReportsOldImportUnavailable,
2188        );
2189
2190        let mut harness = clean_default_harness();
2191        let rename = harness.workspace.rename(DEP, RENAMED_DEP).unwrap();
2192        row.apply_update(&mut harness, &rename);
2193        assert_fs_reason(&harness.compiler);
2194        let artifact = harness.compile_pending();
2195        assert!(artifact.error_cnt() > 0);
2196        assert!(source_is_unavailable(&artifact, &harness.workspace, DEP));
2197        assert_eq!(
2198            source_text(&artifact, &harness.workspace, RENAMED_DEP),
2199            "#let value = [before]"
2200        );
2201    }
2202
2203    fn assert_delete_then_recreate(row: MatrixRow) {
2204        assert_row_shape(
2205            row,
2206            EventVariant::Update,
2207            SyncMode::NonSync,
2208            InsertPayload::NonEmptyContent,
2209            RemovePayload::OneRemovedPath,
2210            &[PathRelation::PreviouslyDependedPath],
2211            BatchShape::RemoveOnlyThenInsertOnly,
2212            SequenceShape::DeleteThenRecreate,
2213            ExpectedOutcome::ReportsThenRecoversRecreatedSource,
2214        );
2215
2216        let mut harness = clean_default_harness();
2217        let removed = harness.workspace.remove(DEP).unwrap();
2218        row.apply_update(&mut harness, &removed);
2219        let artifact = harness.compile_pending();
2220        assert!(artifact.error_cnt() > 0);
2221        assert!(source_is_unavailable(&artifact, &harness.workspace, DEP));
2222        harness.latest_sync_dependencies();
2223
2224        let recreated = harness
2225            .workspace
2226            .create_source(DEP, "#let value = [recreated]");
2227        row.apply_update(&mut harness, &recreated);
2228        assert_fs_reason(&harness.compiler);
2229        let artifact = harness.compile_pending();
2230        assert_eq!(artifact.error_cnt(), 0);
2231        assert_eq!(
2232            source_text(&artifact, &harness.workspace, DEP),
2233            "#let value = [recreated]"
2234        );
2235        let deps = harness.dependency_paths_after_compile();
2236        assert_deps_contain(&harness.workspace, &deps, DEP);
2237    }
2238
2239    fn assert_failed_read_then_recovery(row: MatrixRow) {
2240        assert_row_shape(
2241            row,
2242            EventVariant::Update,
2243            SyncMode::NonSync,
2244            InsertPayload::NonEmptyContent,
2245            RemovePayload::NoRemoves,
2246            &[PathRelation::ImportedDependency],
2247            BatchShape::InsertOnly,
2248            SequenceShape::FailedReadThenRecovery,
2249            ExpectedOutcome::ClearsDiagnosticsAfterRecovery,
2250        );
2251
2252        let mut harness = clean_default_harness();
2253        let read_error = read_error_change(&harness.workspace, DEP);
2254        row.apply_update(&mut harness, &read_error);
2255        let artifact = harness.compile_pending();
2256        assert!(artifact.error_cnt() > 0);
2257        assert!(source_is_unavailable(&artifact, &harness.workspace, DEP));
2258        harness.latest_sync_dependencies();
2259
2260        let recovered = harness
2261            .workspace
2262            .update_source(DEP, "#let value = [recovered]");
2263        row.apply_update(&mut harness, &recovered);
2264        assert_fs_reason(&harness.compiler);
2265        let artifact = harness.compile_pending();
2266        assert_eq!(artifact.error_cnt(), 0);
2267        assert_eq!(
2268            source_text(&artifact, &harness.workspace, DEP),
2269            "#let value = [recovered]"
2270        );
2271        let deps = harness.dependency_paths_after_compile();
2272        assert_deps_contain(&harness.workspace, &deps, DEP);
2273    }
2274
2275    fn assert_rename_batch(row: MatrixRow) {
2276        assert_row_shape(
2277            row,
2278            EventVariant::Update,
2279            SyncMode::NonSync,
2280            InsertPayload::NonEmptyContent,
2281            RemovePayload::OneRemovedPath,
2282            &[
2283                PathRelation::PreviouslyDependedPath,
2284                PathRelation::NewlyReferencedDependency,
2285            ],
2286            BatchShape::RemovePlusInsert,
2287            SequenceShape::RenameOldPlusNew,
2288            ExpectedOutcome::RenameBatchFollowsRenamedPath,
2289        );
2290
2291        let mut harness = clean_default_harness();
2292        let rename = harness.workspace.rename(DEP, RENAMED_DEP).unwrap();
2293        let entry_update = harness
2294            .workspace
2295            .update_source(MAIN, "#import \"renamed.typ\": value\n#value");
2296        let batch = combine_changes(&[rename, entry_update]);
2297        row.apply_update(&mut harness, &batch);
2298        assert_fs_reason(&harness.compiler);
2299        let artifact = harness.compile_pending();
2300        assert_eq!(artifact.error_cnt(), 0);
2301        let deps = harness.dependency_paths_after_compile();
2302        assert_deps_contain(&harness.workspace, &deps, RENAMED_DEP);
2303        assert_deps_do_not_contain(&harness.workspace, &deps, DEP);
2304    }
2305
2306    fn assert_multi_file_unrelated_batch(row: MatrixRow) {
2307        assert_row_shape(
2308            row,
2309            EventVariant::Update,
2310            SyncMode::NonSync,
2311            InsertPayload::NonEmptyContent,
2312            RemovePayload::MultipleRemovedPaths,
2313            &[PathRelation::UnrelatedFile],
2314            BatchShape::MultiFileBatch,
2315            SequenceShape::OneStepEdit,
2316            ExpectedOutcome::MultiFileUnrelatedBatchHarmless,
2317        );
2318
2319        let files = vec![
2320            (MAIN, "#import \"dep.typ\": value\n#value"),
2321            (DEP, "#let value = [before]"),
2322            ("old-a.typ", "#let old_a = [unused]"),
2323            ("old-b.typ", "#let old_b = [unused]"),
2324        ];
2325        let mut harness = ProjectCompilerHarness::new(&files);
2326        let initial = harness.compile_primary();
2327        assert_eq!(initial.error_cnt(), 0);
2328        let deps_before = harness.latest_sync_dependencies();
2329
2330        let remove_a = harness.workspace.remove("old-a.typ").unwrap();
2331        let remove_b = harness.workspace.remove("old-b.typ").unwrap();
2332        let create_a = harness
2333            .workspace
2334            .create_source("new-a.typ", "#let new_a = [unused]");
2335        let create_b = harness
2336            .workspace
2337            .create_source("new-b.typ", "#let new_b = [unused]");
2338        let batch = combine_changes(&[remove_a, remove_b, create_a, create_b]);
2339        row.apply_update(&mut harness, &batch);
2340        assert_fs_reason(&harness.compiler);
2341        let artifact = harness.compile_pending();
2342        assert_eq!(artifact.error_cnt(), 0);
2343        let deps_after = harness.dependency_paths_after_harmless_compile(&deps_before);
2344        assert_eq!(deps_after, deps_before);
2345        assert_deps_do_not_contain(&harness.workspace, &deps_after, "new-a.typ");
2346        assert_deps_do_not_contain(&harness.workspace, &deps_after, "new-b.typ");
2347    }
2348
2349    fn assert_upstream_invalidation(row: MatrixRow) {
2350        assert_row_shape(
2351            row,
2352            EventVariant::UpstreamUpdate,
2353            SyncMode::NotApplicable,
2354            InsertPayload::NonEmptyContent,
2355            RemovePayload::NoRemoves,
2356            &[PathRelation::EntryFile],
2357            BatchShape::InsertOnly,
2358            SequenceShape::DelayedMemoryThenFilesystem,
2359            ExpectedOutcome::AppliesDelayedMemoryBeforeFilesystem,
2360        );
2361
2362        let files = vec![(MAIN, "#let value = [disk]\n#value")];
2363        let mut harness = ProjectCompilerHarness::new(&files);
2364        let initial = harness.compile_primary();
2365        assert_eq!(initial.error_cnt(), 0);
2366        harness.latest_sync_dependencies();
2367
2368        let memory_insert = insert_source_change(
2369            &harness.workspace,
2370            MAIN,
2371            "#let value = [memory shadow]\n#value",
2372        );
2373        harness
2374            .compiler
2375            .process(Interrupt::Memory(memory_insert.memory_event()));
2376        assert_mem_reason(&harness.compiler);
2377        let artifact = harness.compile_pending();
2378        assert_eq!(
2379            source_text(&artifact, &harness.workspace, MAIN),
2380            "#let value = [memory shadow]\n#value"
2381        );
2382        harness.latest_sync_dependencies();
2383
2384        let memory_remove = remove_change(&harness.workspace, MAIN);
2385        harness
2386            .compiler
2387            .process(Interrupt::Memory(memory_remove.memory_event()));
2388        let upstream_event = harness.take_upstream_update();
2389        assert!(
2390            upstream_event
2391                .invalidates
2392                .contains(&harness.workspace.immut_path(MAIN))
2393        );
2394        assert!(
2395            !harness.compiler.primary.reason.any(),
2396            "delayed memory removal should wait for the upstream filesystem event"
2397        );
2398
2399        let filesystem_update = harness
2400            .workspace
2401            .update_source(MAIN, "#let value = [filesystem]\n#value");
2402        harness.apply_upstream_update(filesystem_update.into_changeset(), Some(upstream_event));
2403        assert_fs_reason(&harness.compiler);
2404        assert!(
2405            !harness.compiler.primary.reason.by_mem_events,
2406            "known upstream update should not add a separate memory reason"
2407        );
2408
2409        let artifact = harness.compile_pending();
2410        assert_eq!(artifact.error_cnt(), 0);
2411        assert_eq!(
2412            source_text(&artifact, &harness.workspace, MAIN),
2413            "#let value = [filesystem]\n#value"
2414        );
2415    }
2416
2417    fn assert_unrelated_churn(row: MatrixRow) {
2418        assert_row_shape(
2419            row,
2420            EventVariant::Update,
2421            SyncMode::NonSync,
2422            InsertPayload::NonEmptyContent,
2423            RemovePayload::NoRemoves,
2424            &[PathRelation::UnrelatedFile],
2425            BatchShape::InsertOnly,
2426            SequenceShape::OneStepEdit,
2427            ExpectedOutcome::KeepsUnrelatedChurnHarmless,
2428        );
2429
2430        let (mut harness, deps_before) = clean_default_harness_with_deps();
2431        let unrelated = harness
2432            .workspace
2433            .update_source(UNRELATED, "#let note = [changed but unused]");
2434        row.apply_update(&mut harness, &unrelated);
2435        assert_fs_reason(&harness.compiler);
2436        let artifact = harness.compile_pending();
2437        assert_eq!(artifact.error_cnt(), 0);
2438        let deps_after = harness.dependency_paths_after_harmless_compile(&deps_before);
2439        assert_eq!(deps_after, deps_before);
2440        assert_deps_do_not_contain(&harness.workspace, &deps_after, UNRELATED);
2441    }
2442
2443    fn assert_empty_changeset(row: MatrixRow) {
2444        assert_row_shape(
2445            row,
2446            EventVariant::Update,
2447            SyncMode::NonSync,
2448            InsertPayload::NoInserts,
2449            RemovePayload::NoRemoves,
2450            &[PathRelation::UnrelatedFile],
2451            BatchShape::EmptyChangeset,
2452            SequenceShape::EmptyChangeset,
2453            ExpectedOutcome::ExplicitNoContentOutcome,
2454        );
2455
2456        let (mut harness, deps_before) = clean_default_harness_with_deps();
2457        let empty = empty_change();
2458        row.apply_update(&mut harness, &empty);
2459        assert_fs_reason(&harness.compiler);
2460        let artifact = harness.compile_pending();
2461        assert_eq!(artifact.error_cnt(), 0);
2462        let deps_after = harness.dependency_paths_after_harmless_compile(&deps_before);
2463        assert_eq!(deps_after, deps_before);
2464    }
2465}