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