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