reflexo_world/
world.rs

1use std::{
2    num::NonZeroUsize,
3    ops::Deref,
4    path::{Path, PathBuf},
5    sync::{Arc, LazyLock, OnceLock},
6};
7
8use chrono::{DateTime, Datelike, Local};
9use parking_lot::RwLock;
10use reflexo::error::prelude::*;
11use reflexo::ImmutPath;
12use reflexo_vfs::{notify::FilesystemEvent, Vfs};
13use typst::{
14    diag::{eco_format, At, EcoString, FileError, FileResult, SourceResult},
15    foundations::{Bytes, Datetime, Dict},
16    syntax::{FileId, Source, Span},
17    text::{Font, FontBook},
18    utils::LazyHash,
19    Library, World,
20};
21
22use crate::{
23    entry::{EntryManager, EntryReader, EntryState, DETACHED_ENTRY},
24    font::FontResolver,
25    package::{PackageRegistry, PackageSpec},
26    parser::{
27        get_semantic_tokens_full, get_semantic_tokens_legend, OffsetEncoding, SemanticToken,
28        SemanticTokensLegend,
29    },
30    source::{SharedState, SourceCache, SourceDb},
31    CodespanError, CodespanResult, CompilerFeat, ShadowApi, WorldDeps,
32};
33
34pub struct Revising<'a, T> {
35    pub revision: NonZeroUsize,
36    pub inner: &'a mut T,
37}
38
39impl<T> std::ops::Deref for Revising<'_, T> {
40    type Target = T;
41
42    fn deref(&self) -> &Self::Target {
43        self.inner
44    }
45}
46
47impl<T> std::ops::DerefMut for Revising<'_, T> {
48    fn deref_mut(&mut self) -> &mut Self::Target {
49        self.inner
50    }
51}
52
53impl<F: CompilerFeat> Revising<'_, CompilerUniverse<F>> {
54    pub fn vfs(&mut self) -> &mut Vfs<F::AccessModel> {
55        &mut self.inner.vfs
56    }
57
58    /// Let the vfs notify the access model with a filesystem event.
59    ///
60    /// See `reflexo_vfs::NotifyAccessModel` for more information.
61    pub fn notify_fs_event(&mut self, event: FilesystemEvent) {
62        self.inner.vfs.notify_fs_event(event);
63    }
64
65    pub fn reset_shadow(&mut self) {
66        self.inner.vfs.reset_shadow()
67    }
68
69    pub fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> {
70        self.inner.vfs.map_shadow(path, content)
71    }
72
73    pub fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> {
74        self.inner.vfs.remove_shadow(path);
75        Ok(())
76    }
77
78    /// Set the `do_reparse` flag.
79    pub fn set_do_reparse(&mut self, do_reparse: bool) {
80        self.inner.do_reparse = do_reparse;
81    }
82
83    /// Set the inputs for the compiler.
84    pub fn set_inputs(&mut self, inputs: Arc<LazyHash<Dict>>) {
85        self.inner.inputs = inputs;
86    }
87
88    pub fn set_entry_file(&mut self, entry_file: Arc<Path>) -> SourceResult<()> {
89        self.inner.set_entry_file_(entry_file)
90    }
91
92    pub fn mutate_entry(&mut self, state: EntryState) -> SourceResult<EntryState> {
93        self.inner.mutate_entry_(state)
94    }
95}
96
97/// A universe that provides access to the operating system.
98///
99/// Use [`CompilerUniverse::new`] to create a new universe.
100/// Use [`CompilerUniverse::snapshot`] to create a new world.
101#[derive(Debug)]
102pub struct CompilerUniverse<F: CompilerFeat> {
103    /// State for the *root & entry* of compilation.
104    /// The world forbids direct access to files outside this directory.
105    entry: EntryState,
106    /// Additional input arguments to compile the entry file.
107    inputs: Arc<LazyHash<Dict>>,
108    /// Whether to reparse the source files.
109    do_reparse: bool,
110
111    /// Provides font management for typst compiler.
112    pub font_resolver: Arc<F::FontResolver>,
113    /// Provides package management for typst compiler.
114    pub registry: Arc<F::Registry>,
115    /// Provides path-based data access for typst compiler.
116    vfs: Vfs<F::AccessModel>,
117
118    /// The current revision of the source database.
119    pub revision: RwLock<NonZeroUsize>,
120    /// Shared state for source cache.
121    pub shared: Arc<RwLock<SharedState<SourceCache>>>,
122}
123
124/// Creates, snapshots, and manages the compiler universe.
125impl<F: CompilerFeat> CompilerUniverse<F> {
126    /// Create a [`CompilerUniverse`] with feature implementation.
127    ///
128    /// Although this function is public, it is always unstable and not intended
129    /// to be used directly.
130    /// + See [`crate::TypstSystemUniverse::new`] for system environment.
131    /// + See [`crate::TypstBrowserUniverse::new`] for browser environment.
132    pub fn new_raw(
133        entry: EntryState,
134        inputs: Option<Arc<LazyHash<Dict>>>,
135        vfs: Vfs<F::AccessModel>,
136        registry: F::Registry,
137        font_resolver: Arc<F::FontResolver>,
138    ) -> Self {
139        Self {
140            entry,
141            inputs: inputs.unwrap_or_default(),
142            do_reparse: true,
143
144            revision: RwLock::new(NonZeroUsize::new(1).expect("initial revision is 1")),
145            shared: Arc::new(RwLock::new(SharedState::default())),
146
147            font_resolver,
148            registry: Arc::new(registry),
149            vfs,
150        }
151    }
152
153    /// Wrap driver with a given entry file.
154    pub fn with_entry_file(mut self, entry_file: PathBuf) -> Self {
155        let _ = self.increment_revision(|this| this.set_entry_file_(entry_file.as_path().into()));
156        self
157    }
158
159    pub fn do_reparse(&self) -> bool {
160        self.do_reparse
161    }
162
163    pub fn inputs(&self) -> Arc<LazyHash<Dict>> {
164        self.inputs.clone()
165    }
166
167    pub fn snapshot(&self) -> CompilerWorld<F> {
168        self.snapshot_with(None)
169    }
170
171    pub fn snapshot_with(&self, mutant: Option<TaskInputs>) -> CompilerWorld<F> {
172        let rev_lock = self.revision.read();
173
174        let w = CompilerWorld {
175            entry: self.entry.clone(),
176            inputs: self.inputs.clone(),
177            library: create_library(self.inputs.clone()),
178            font_resolver: self.font_resolver.clone(),
179            registry: self.registry.clone(),
180            vfs: self.vfs.snapshot(),
181            source_db: SourceDb {
182                revision: *rev_lock,
183                do_reparse: self.do_reparse,
184                shared: self.shared.clone(),
185                slots: Default::default(),
186            },
187            now: OnceLock::new(),
188        };
189
190        mutant.map(|m| w.task(m)).unwrap_or(w)
191    }
192
193    /// Increment revision with actions.
194    pub fn increment_revision<T>(&mut self, f: impl FnOnce(&mut Revising<Self>) -> T) -> T {
195        let rev_lock = self.revision.get_mut();
196        *rev_lock = rev_lock.checked_add(1).unwrap();
197        let revision = *rev_lock;
198        f(&mut Revising {
199            inner: self,
200            revision,
201        })
202    }
203
204    /// Mutate the entry state and return the old state.
205    fn mutate_entry_(&mut self, mut state: EntryState) -> SourceResult<EntryState> {
206        self.reset();
207        std::mem::swap(&mut self.entry, &mut state);
208        Ok(state)
209    }
210
211    /// set an entry file.
212    fn set_entry_file_(&mut self, entry_file: Arc<Path>) -> SourceResult<()> {
213        let state = self.entry_state();
214        let state = state
215            .try_select_path_in_workspace(&entry_file, true)
216            .map_err(|e| eco_format!("cannot select entry file out of workspace: {e}"))
217            .at(Span::detached())?
218            .ok_or_else(|| eco_format!("failed to determine root"))
219            .at(Span::detached())?;
220
221        self.mutate_entry_(state).map(|_| ())?;
222        Ok(())
223    }
224}
225
226impl<F: CompilerFeat> CompilerUniverse<F> {
227    /// Reset the world for a new lifecycle (of garbage collection).
228    pub fn reset(&mut self) {
229        self.vfs.reset();
230        // todo: shared state
231    }
232
233    /// Resolve the real path for a file id.
234    pub fn path_for_id(&self, id: FileId) -> Result<PathBuf, FileError> {
235        if id == *DETACHED_ENTRY {
236            return Ok(DETACHED_ENTRY.vpath().as_rooted_path().to_owned());
237        }
238
239        // Determine the root path relative to which the file path
240        // will be resolved.
241        let root = match id.package() {
242            Some(spec) => self.registry.resolve(spec)?,
243            None => self.entry.root().ok_or(FileError::Other(Some(eco_format!(
244                "cannot access directory without root: state: {:?}",
245                self.entry
246            ))))?,
247        };
248
249        // Join the path to the root. If it tries to escape, deny
250        // access. Note: It can still escape via symlinks.
251        id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
252    }
253
254    pub fn get_semantic_token_legend(&self) -> Arc<SemanticTokensLegend> {
255        Arc::new(get_semantic_tokens_legend())
256    }
257
258    pub fn get_semantic_tokens(
259        &self,
260        file_path: Option<String>,
261        encoding: OffsetEncoding,
262    ) -> ZResult<Arc<Vec<SemanticToken>>> {
263        let world = match file_path {
264            Some(e) => {
265                let path = Path::new(&e);
266                let s = self
267                    .entry_state()
268                    .try_select_path_in_workspace(path, true)?
269                    .ok_or_else(|| error_once!("cannot select file", path: e))?;
270
271                self.snapshot_with(Some(TaskInputs {
272                    entry: Some(s),
273                    inputs: None,
274                }))
275            }
276            None => self.snapshot(),
277        };
278
279        let src = world
280            .source(world.main())
281            .map_err(|e| error_once!("cannot access source file", err: e))?;
282        Ok(Arc::new(get_semantic_tokens_full(&src, encoding)))
283    }
284}
285
286impl<F: CompilerFeat> ShadowApi for CompilerUniverse<F> {
287    #[inline]
288    fn _shadow_map_id(&self, file_id: FileId) -> FileResult<PathBuf> {
289        self.path_for_id(file_id)
290    }
291
292    #[inline]
293    fn shadow_paths(&self) -> Vec<Arc<Path>> {
294        self.vfs.shadow_paths()
295    }
296
297    #[inline]
298    fn reset_shadow(&mut self) {
299        self.increment_revision(|this| this.vfs.reset_shadow())
300    }
301
302    #[inline]
303    fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> {
304        self.increment_revision(|this| this.vfs().map_shadow(path, content))
305    }
306
307    #[inline]
308    fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> {
309        self.increment_revision(|this| {
310            this.vfs().remove_shadow(path);
311            Ok(())
312        })
313    }
314}
315
316impl<F: CompilerFeat> EntryReader for CompilerUniverse<F> {
317    fn entry_state(&self) -> EntryState {
318        self.entry.clone()
319    }
320}
321
322impl<F: CompilerFeat> EntryManager for CompilerUniverse<F> {
323    fn reset(&mut self) -> SourceResult<()> {
324        self.reset();
325        Ok(())
326    }
327
328    fn mutate_entry(&mut self, state: EntryState) -> SourceResult<EntryState> {
329        self.increment_revision(|this| this.mutate_entry_(state))
330    }
331}
332
333pub struct CompilerWorld<F: CompilerFeat> {
334    /// State for the *root & entry* of compilation.
335    /// The world forbids direct access to files outside this directory.
336    entry: EntryState,
337    /// Additional input arguments to compile the entry file.
338    inputs: Arc<LazyHash<Dict>>,
339
340    /// Provides library for typst compiler.
341    pub library: Arc<LazyHash<Library>>,
342    /// Provides font management for typst compiler.
343    pub font_resolver: Arc<F::FontResolver>,
344    /// Provides package management for typst compiler.
345    pub registry: Arc<F::Registry>,
346    /// Provides path-based data access for typst compiler.
347    vfs: Vfs<F::AccessModel>,
348
349    /// Provides source database for typst compiler.
350    pub source_db: SourceDb,
351    /// The current datetime if requested. This is stored here to ensure it is
352    /// always the same within one compilation. Reset between compilations.
353    now: OnceLock<DateTime<Local>>,
354}
355
356impl<F: CompilerFeat> Clone for CompilerWorld<F> {
357    fn clone(&self) -> Self {
358        self.task(TaskInputs::default())
359    }
360}
361
362impl<F: CompilerFeat> Drop for CompilerWorld<F> {
363    fn drop(&mut self) {
364        let state = self.source_db.shared.clone();
365        let source_state = self.source_db.take_state();
366        let mut state = state.write();
367        source_state.commit_impl(&mut state);
368    }
369}
370
371#[derive(Default)]
372pub struct TaskInputs {
373    pub entry: Option<EntryState>,
374    pub inputs: Option<Arc<LazyHash<Dict>>>,
375}
376
377impl<F: CompilerFeat> CompilerWorld<F> {
378    pub fn task(&self, mutant: TaskInputs) -> CompilerWorld<F> {
379        // Fetch to avoid inconsistent state.
380        let _ = self.today(None);
381
382        let library = mutant.inputs.clone().map(create_library);
383
384        CompilerWorld {
385            inputs: mutant.inputs.unwrap_or_else(|| self.inputs.clone()),
386            library: library.unwrap_or_else(|| self.library.clone()),
387            entry: mutant.entry.unwrap_or_else(|| self.entry.clone()),
388            font_resolver: self.font_resolver.clone(),
389            registry: self.registry.clone(),
390            vfs: self.vfs.snapshot(),
391            source_db: self.source_db.clone(),
392            now: self.now.clone(),
393        }
394    }
395
396    pub fn inputs(&self) -> Arc<LazyHash<Dict>> {
397        self.inputs.clone()
398    }
399
400    /// Resolve the real path for a file id.
401    pub fn path_for_id(&self, id: FileId) -> Result<PathBuf, FileError> {
402        if id == *DETACHED_ENTRY {
403            return Ok(DETACHED_ENTRY.vpath().as_rooted_path().to_owned());
404        }
405
406        // Determine the root path relative to which the file path
407        // will be resolved.
408        let root = match id.package() {
409            Some(spec) => self.registry.resolve(spec)?,
410            None => self.entry.root().ok_or(FileError::Other(Some(eco_format!(
411                "cannot access directory without root: state: {:?}",
412                self.entry
413            ))))?,
414        };
415
416        // Join the path to the root. If it tries to escape, deny
417        // access. Note: It can still escape via symlinks.
418        id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
419    }
420    /// Lookup a source file by id.
421    #[track_caller]
422    fn lookup(&self, id: FileId) -> Source {
423        self.source(id)
424            .expect("file id does not point to any source file")
425    }
426
427    fn map_source_or_default<T>(
428        &self,
429        id: FileId,
430        default_v: T,
431        f: impl FnOnce(Source) -> CodespanResult<T>,
432    ) -> CodespanResult<T> {
433        match World::source(self, id).ok() {
434            Some(source) => f(source),
435            None => Ok(default_v),
436        }
437    }
438
439    pub fn revision(&self) -> NonZeroUsize {
440        self.source_db.revision
441    }
442}
443
444impl<F: CompilerFeat> ShadowApi for CompilerWorld<F> {
445    #[inline]
446    fn _shadow_map_id(&self, file_id: FileId) -> FileResult<PathBuf> {
447        self.path_for_id(file_id)
448    }
449
450    #[inline]
451    fn shadow_paths(&self) -> Vec<Arc<Path>> {
452        self.vfs.shadow_paths()
453    }
454
455    #[inline]
456    fn reset_shadow(&mut self) {
457        self.vfs.reset_shadow()
458    }
459
460    #[inline]
461    fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> {
462        self.vfs.map_shadow(path, content)
463    }
464
465    #[inline]
466    fn unmap_shadow(&mut self, path: &Path) -> FileResult<()> {
467        self.vfs.remove_shadow(path);
468        Ok(())
469    }
470}
471
472impl<F: CompilerFeat> World for CompilerWorld<F> {
473    /// The standard library.
474    fn library(&self) -> &LazyHash<Library> {
475        self.library.as_ref()
476    }
477
478    /// Access the main source file.
479    fn main(&self) -> FileId {
480        self.entry.main().unwrap_or_else(|| *DETACHED_ENTRY)
481    }
482
483    /// Metadata about all known fonts.
484    fn font(&self, id: usize) -> Option<Font> {
485        self.font_resolver.font(id)
486    }
487
488    /// Try to access the specified file.
489    fn book(&self) -> &LazyHash<FontBook> {
490        self.font_resolver.font_book()
491    }
492
493    /// Try to access the specified source file.
494    ///
495    /// The returned `Source` file's [id](Source::id) does not have to match the
496    /// given `id`. Due to symlinks, two different file id's can point to the
497    /// same on-disk file. Implementors can deduplicate and return the same
498    /// `Source` if they want to, but do not have to.
499    fn source(&self, id: FileId) -> FileResult<Source> {
500        static DETACH_SOURCE: LazyLock<Source> =
501            LazyLock::new(|| Source::new(*DETACHED_ENTRY, String::new()));
502
503        if id == *DETACHED_ENTRY {
504            return Ok(DETACH_SOURCE.clone());
505        }
506
507        let fid = self.vfs.file_id(&self.path_for_id(id)?);
508        self.source_db.source(id, fid, &self.vfs)
509    }
510
511    /// Try to access the specified file.
512    fn file(&self, id: FileId) -> FileResult<Bytes> {
513        let fid = self.vfs.file_id(&self.path_for_id(id)?);
514        self.source_db.file(id, fid, &self.vfs)
515    }
516
517    /// Get the current date.
518    ///
519    /// If no offset is specified, the local date should be chosen. Otherwise,
520    /// the UTC date should be chosen with the corresponding offset in hours.
521    ///
522    /// If this function returns `None`, Typst's `datetime` function will
523    /// return an error.
524    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
525        let now = self.now.get_or_init(|| reflexo::time::now().into());
526
527        let naive = match offset {
528            None => now.naive_local(),
529            Some(o) => now.naive_utc() + chrono::Duration::try_hours(o)?,
530        };
531
532        Datetime::from_ymd(
533            naive.year(),
534            naive.month().try_into().ok()?,
535            naive.day().try_into().ok()?,
536        )
537    }
538
539    /// A list of all available packages and optionally descriptions for them.
540    ///
541    /// This function is optional to implement. It enhances the user experience
542    /// by enabling autocompletion for packages. Details about packages from the
543    /// `@preview` namespace are available from
544    /// `https://packages.typst.org/preview/index.json`.
545    fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
546        self.registry.packages()
547    }
548}
549
550impl<F: CompilerFeat> EntryReader for CompilerWorld<F> {
551    fn entry_state(&self) -> EntryState {
552        self.entry.clone()
553    }
554}
555
556impl<F: CompilerFeat> WorldDeps for CompilerWorld<F> {
557    #[inline]
558    fn iter_dependencies(&self, f: &mut dyn FnMut(ImmutPath)) {
559        self.source_db.iter_dependencies_dyn(&self.vfs, f)
560    }
561}
562
563impl<'a, F: CompilerFeat> codespan_reporting::files::Files<'a> for CompilerWorld<F> {
564    /// A unique identifier for files in the file provider. This will be used
565    /// for rendering `diagnostic::Label`s in the corresponding source files.
566    type FileId = FileId;
567
568    /// The user-facing name of a file, to be displayed in diagnostics.
569    type Name = String;
570
571    /// The source code of a file.
572    type Source = Source;
573
574    /// The user-facing name of a file.
575    fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
576        let vpath = id.vpath();
577        Ok(if let Some(package) = id.package() {
578            format!("{package}{}", vpath.as_rooted_path().display())
579        } else {
580            match self.entry.root() {
581                Some(root) => {
582                    // Try to express the path relative to the working directory.
583                    vpath
584                        .resolve(&root)
585                        // differ from typst
586                        // .and_then(|abs| pathdiff::diff_paths(&abs, self.workdir()))
587                        .as_deref()
588                        .unwrap_or_else(|| vpath.as_rootless_path())
589                        .to_string_lossy()
590                        .into()
591                }
592                None => vpath.as_rooted_path().display().to_string(),
593            }
594        })
595    }
596
597    /// The source code of a file.
598    fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
599        Ok(self.lookup(id))
600    }
601
602    /// See [`codespan_reporting::files::Files::line_index`].
603    fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
604        let source = self.lookup(id);
605        source
606            .byte_to_line(given)
607            .ok_or_else(|| CodespanError::IndexTooLarge {
608                given,
609                max: source.len_bytes(),
610            })
611    }
612
613    /// See [`codespan_reporting::files::Files::column_number`].
614    fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult<usize> {
615        let source = self.lookup(id);
616        source.byte_to_column(given).ok_or_else(|| {
617            let max = source.len_bytes();
618            if given <= max {
619                CodespanError::InvalidCharBoundary { given }
620            } else {
621                CodespanError::IndexTooLarge { given, max }
622            }
623        })
624    }
625
626    /// See [`codespan_reporting::files::Files::line_range`].
627    fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult<std::ops::Range<usize>> {
628        self.map_source_or_default(id, 0..0, |source| {
629            source
630                .line_to_range(given)
631                .ok_or_else(|| CodespanError::LineTooLarge {
632                    given,
633                    max: source.len_lines(),
634                })
635        })
636    }
637}
638
639#[comemo::memoize]
640fn create_library(inputs: Arc<LazyHash<Dict>>) -> Arc<LazyHash<Library>> {
641    let lib = typst::Library::builder()
642        .with_inputs(inputs.deref().deref().clone())
643        .build();
644
645    Arc::new(LazyHash::new(lib))
646}