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