1use 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
26pub struct CompiledArtifact<F: CompilerFeat> {
28 pub graph: Arc<WorldComputeGraph<F>>,
30 pub diag: Arc<DiagnosticsTask>,
32 pub doc: Option<TypstDocument>,
34 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 pub fn id(&self) -> &ProjectInsId {
67 &self.graph.snap.id
68 }
69
70 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 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 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 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 pub fn error_cnt(&self) -> usize {
121 self.diag.error_cnt()
122 }
123
124 pub fn warning_cnt(&self) -> usize {
126 self.diag.warning_cnt()
127 }
128
129 pub fn diagnostics(&self) -> impl Iterator<Item = &typst::diag::SourceDiagnostic> + Clone {
131 self.diag.diagnostics()
132 }
133
134 pub fn has_errors(&self) -> bool {
136 self.error_cnt() > 0
137 }
138
139 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#[derive(Debug, Clone)]
151pub struct CompileReport {
152 pub id: ProjectInsId,
154 pub compiling_id: Option<FileId>,
156 pub page_count: u32,
158 pub status: CompileStatusEnum,
160}
161
162#[derive(Debug, Clone)]
164pub enum CompileStatusEnum {
165 Suspend,
167 Compiling,
169 CompileSuccess(CompileStatusResult),
171 CompileError(CompileStatusResult),
173 ExportError(CompileStatusResult),
175}
176
177#[derive(Debug, Clone)]
179pub struct CompileStatusResult {
180 diag: u32,
182 elapsed: tinymist_std::time::Duration,
184}
185
186impl CompileReport {
187 pub fn message(&self) -> CompileReportMsg<'_> {
189 CompileReportMsg(self)
190 }
191}
192
193pub 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
219pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
221 fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
224 fn notify_compile(&self, res: &CompiledArtifact<F>);
227 fn notify_removed(&self, _id: &ProjectInsId) {}
229 fn status(&self, revision: usize, rep: CompileReport);
231}
232
233impl<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
244pub enum Interrupt<F: CompilerFeat> {
246 Compile(ProjectInsId),
248 Settle(ProjectInsId),
250 Compiled(CompiledArtifact<F>),
252 ChangeTask(ProjectInsId, TaskInputs),
254 Font(Arc<F::FontResolver>),
256 CreationTimestamp(Option<i64>),
258 Memory(MemoryEvent),
260 Fs(FilesystemEvent),
262 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
309struct TaggedMemoryEvent {
311 logical_tick: usize,
313 event: MemoryEvent,
315}
316
317pub struct CompileServerOpts<F: CompilerFeat, Ext> {
319 pub handler: Arc<dyn CompileHandler<F, Ext>>,
321 pub ignore_first_sync: bool,
323 pub export_target: ExportTarget,
325 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");
341pub const FILE_MISSING_ERROR: FileError = FileError::Other(Some(FILE_MISSING_ERROR_MSG));
343
344pub struct ProjectCompiler<F: CompilerFeat, Ext> {
346 pub handler: Arc<dyn CompileHandler<F, Ext>>,
348 export_target: ExportTarget,
350 syntax_only: bool,
352 dep_tx: mpsc::UnboundedSender<NotifyMessage>,
354 pub ignore_first_sync: bool,
356
357 logical_tick: usize,
359 dirty_shadow_logical_tick: usize,
361 estimated_shadow_files: HashSet<Arc<Path>>,
363
364 pub primary: ProjectInsState<F, Ext>,
366 pub dedicates: Vec<ProjectInsState<F, Ext>>,
368 deps: ProjectDeps,
370}
371
372impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
373 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 pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
411 self.primary.snapshot()
412 }
413
414 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 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 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 pub fn clear_dedicates(&mut self) {
468 self.dedicates.clear();
469 }
470
471 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 self.handler.notify_removed(id);
505 self.deps.project_deps.remove_mut(id);
506
507 let _proj = self.dedicates.remove(idx);
508 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 pub fn process(&mut self, intr: Interrupt<F>) {
521 self.process_inner(intr);
523 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 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 if let Some(entry) = change.entry {
574 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(event) = event {
741 let TaggedMemoryEvent {
742 logical_tick,
743 event,
744 } = event.opaque.as_ref().downcast_ref()?;
745
746 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 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
775pub struct ProjectInsState<F: CompilerFeat, Ext> {
777 pub id: ProjectInsId,
779 pub ext: Ext,
781 pub verse: CompilerUniverse<F>,
783 pub export_target: ExportTarget,
785 pub syntax_only: bool,
787 pub reason: CompileSignal,
789 pub handler: Arc<dyn CompileHandler<F, Ext>>,
791 deps: EcoVec<ImmutPath>,
793
794 pub cached_snapshot: Option<Arc<WorldComputeGraph<F>>>,
797 pub latest_compilation: OnceLock<CompiledArtifact<F>>,
799 pub latest_success_doc: Option<TypstDocument>,
801
802 committed_revision: usize,
803}
804
805impl<F: CompilerFeat, Ext: 'static> ProjectInsState<F, Ext> {
806 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 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 #[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 #[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 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 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 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 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; 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 spawn_cpu(move || {
983 let evict_start = tinymist_std::time::Instant::now();
984 if is_primary {
985 comemo::evict(10);
986
987 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#[cfg(not(target_arch = "wasm32"))]
1025pub fn spawn_cpu<F>(func: F)
1027where
1028 F: FnOnce() + Send + 'static,
1029{
1030 rayon::spawn(func);
1031}
1032
1033#[cfg(target_arch = "wasm32")]
1034pub 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}