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