diff --git a/Cargo.lock b/Cargo.lock index 407ff66ad..ccaf93910 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -89,6 +98,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + [[package]] name = "bytemuck" version = "1.12.1" @@ -107,6 +122,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "winapi", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -123,6 +150,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "comemo" +version = "0.1.0" +dependencies = [ + "comemo-macros", + "siphasher", +] + +[[package]] +name = "comemo-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "crc32fast" version = "1.3.2" @@ -132,6 +182,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "csv" version = "1.1.6" @@ -208,6 +278,18 @@ dependencies = [ "regex", ] +[[package]] +name = "filetime" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + [[package]] name = "flate2" version = "1.0.24" @@ -230,6 +312,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -274,6 +365,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "iana-time-zone" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "once_cell", + "wasm-bindgen", + "winapi", +] + [[package]] name = "image" version = "0.24.3" @@ -290,6 +395,26 @@ dependencies = [ "png", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.9.0" @@ -317,6 +442,35 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "kurbo" version = "0.8.3" @@ -399,6 +553,18 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "nom" version = "5.1.2" @@ -410,6 +576,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "notify" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -850,7 +1034,9 @@ version = "0.1.0" dependencies = [ "bitflags", "bytemuck", + "chrono", "codespan-reporting", + "comemo", "csv", "dirs", "elsa", @@ -863,6 +1049,7 @@ dependencies = [ "lipsum", "memmap2", "miniz_oxide", + "notify", "once_cell", "pdf-writer", "pico-args", @@ -1012,6 +1199,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "weezl" version = "0.1.7" @@ -1049,6 +1290,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "xi-unicode" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index b56e8991c..65afa214d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,20 +4,6 @@ version = "0.1.0" authors = ["The Typst Project Developers"] edition = "2021" -[features] -default = ["tests"] -tests = ["same-file", "walkdir", "elsa", "siphasher"] -cli = [ - "pico-args", - "codespan-reporting", - "dirs", - "memmap2", - "same-file", - "walkdir", - "elsa", - "siphasher", -] - [dependencies] # Workspace typst-macros = { path = "./macros" } @@ -26,13 +12,15 @@ typst-macros = { path = "./macros" } bitflags = "1" bytemuck = "1" fxhash = "0.2" -lipsum = { git = "https://github.com/reknih/lipsum" } once_cell = "1" serde = { version = "1", features = ["derive"] } typed-arena = "2" unscanny = "0.1" regex = "1" +# Incremental compilation +comemo = { path = "../comemo" } + # Text and font handling hypher = "0.1" kurbo = "0.8" @@ -51,6 +39,7 @@ usvg = { version = "0.22", default-features = false } # External implementation of user-facing features syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } rex = { git = "https://github.com/laurmaedje/ReX" } +lipsum = { git = "https://github.com/reknih/lipsum" } csv = "1" # PDF export @@ -75,11 +64,29 @@ elsa = { version = "1.7", optional = true } dirs = { version = "4", optional = true } memmap2 = { version = "0.5", optional = true } siphasher = { version = "0.3", optional = true } +notify = { version = "5", optional = true } +chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } [dev-dependencies] iai = { git = "https://github.com/reknih/iai" } walkdir = "2" +[features] +default = ["tests"] +tests = ["same-file", "walkdir", "elsa", "siphasher"] +cli = [ + "pico-args", + "codespan-reporting", + "dirs", + "memmap2", + "same-file", + "walkdir", + "elsa", + "siphasher", + "notify", + "chrono", +] + [profile.dev] # Faster compilation debug = 0 diff --git a/benches/oneshot.rs b/benches/oneshot.rs index 50fee9b18..23f829b3a 100644 --- a/benches/oneshot.rs +++ b/benches/oneshot.rs @@ -1,5 +1,6 @@ use std::path::Path; +use comemo::{Prehashed, Track, Tracked}; use iai::{black_box, main, Iai}; use unscanny::Scanner; @@ -76,14 +77,16 @@ fn bench_highlight(iai: &mut Iai) { fn bench_eval(iai: &mut Iai) { let world = BenchWorld::new(); let id = world.source.id(); - iai.run(|| typst::eval::evaluate(&world, id, vec![]).unwrap()); + let route = typst::eval::Route::default(); + iai.run(|| typst::eval::eval(world.track(), route.track(), id).unwrap()); } fn bench_layout(iai: &mut Iai) { let world = BenchWorld::new(); let id = world.source.id(); - let module = typst::eval::evaluate(&world, id, vec![]).unwrap(); - iai.run(|| typst::model::layout(&world, &module.content)); + let route = typst::eval::Route::default(); + let module = typst::eval::eval(world.track(), route.track(), id).unwrap(); + iai.run(|| typst::model::layout(world.track(), &module.content)); } fn bench_render(iai: &mut Iai) { @@ -94,41 +97,38 @@ fn bench_render(iai: &mut Iai) { } struct BenchWorld { - config: Config, - book: FontBook, + config: Prehashed, + book: Prehashed, font: Font, source: Source, } impl BenchWorld { fn new() -> Self { + let config = Config::default(); let font = Font::new(FONT.into(), 0).unwrap(); let book = FontBook::from_fonts([&font]); - let id = SourceId::from_raw(0); + let id = SourceId::from_u16(0); let source = Source::new(id, Path::new("bench.typ"), TEXT.into()); Self { - config: Config::default(), - book, + config: Prehashed::new(config), + book: Prehashed::new(book), font, source, } } + + fn track(&self) -> Tracked { + (self as &dyn World).track() + } } impl World for BenchWorld { - fn config(&self) -> &Config { + fn config(&self) -> &Prehashed { &self.config } - fn resolve(&self, path: &Path) -> FileResult { - Err(FileError::NotFound(path.into())) - } - - fn source(&self, _: SourceId) -> &Source { - &self.source - } - - fn book(&self) -> &FontBook { + fn book(&self) -> &Prehashed { &self.book } @@ -139,4 +139,12 @@ impl World for BenchWorld { fn file(&self, path: &Path) -> FileResult { Err(FileError::NotFound(path.into())) } + + fn resolve(&self, path: &Path) -> FileResult { + Err(FileError::NotFound(path.into())) + } + + fn source(&self, _: SourceId) -> &Source { + &self.source + } } diff --git a/src/diag.rs b/src/diag.rs index ebd192c23..81bb7e517 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -3,6 +3,9 @@ use std::fmt::{self, Display, Formatter}; use std::io; use std::path::{Path, PathBuf}; +use std::string::FromUtf8Error; + +use comemo::Tracked; use crate::syntax::{Span, Spanned}; use crate::World; @@ -84,13 +87,13 @@ impl Display for Tracepoint { /// Enrich a [`SourceResult`] with a tracepoint. pub trait Trace { /// Add the tracepoint to all errors that lie outside the `span`. - fn trace(self, world: &dyn World, make_point: F, span: Span) -> Self + fn trace(self, world: Tracked, make_point: F, span: Span) -> Self where F: Fn() -> Tracepoint; } impl Trace for SourceResult { - fn trace(self, world: &dyn World, make_point: F, span: Span) -> Self + fn trace(self, world: Tracked, make_point: F, span: Span) -> Self where F: Fn() -> Tracepoint, { @@ -146,6 +149,8 @@ pub type FileResult = Result; pub enum FileError { /// A file was not found at this path. NotFound(PathBuf), + /// A directory was found, but a file was expected. + IsDirectory, /// A file could not be accessed. AccessDenied, /// The file was not valid UTF-8, but should have been. @@ -178,13 +183,20 @@ impl Display for FileError { Self::NotFound(path) => { write!(f, "file not found (searched at {})", path.display()) } - Self::AccessDenied => f.pad("file access denied"), + Self::IsDirectory => f.pad("failed to load file (is a directory)"), + Self::AccessDenied => f.pad("failed to load file (access denied)"), Self::InvalidUtf8 => f.pad("file is not valid utf-8"), Self::Other => f.pad("failed to load file"), } } } +impl From for FileError { + fn from(_: FromUtf8Error) -> Self { + Self::InvalidUtf8 + } +} + impl From for String { fn from(error: FileError) -> Self { error.to_string() diff --git a/src/eval/func.rs b/src/eval/func.rs index b8730a8a9..c307b2373 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -2,7 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use super::{Args, Eval, Flow, Scope, Scopes, Value, Vm}; +use comemo::{Track, Tracked}; + +use super::{Args, Eval, Flow, Route, Scope, Scopes, Value, Vm}; use crate::diag::{SourceResult, StrResult}; use crate::model::{Content, NodeId, StyleMap}; use crate::source::SourceId; @@ -100,8 +102,13 @@ impl Func { } /// Call the function without an existing virtual machine. - pub fn call_detached(&self, world: &dyn World, args: Args) -> SourceResult { - let mut vm = Vm::new(world, vec![], Scopes::new(None)); + pub fn call_detached( + &self, + world: Tracked, + args: Args, + ) -> SourceResult { + let route = Route::default(); + let mut vm = Vm::new(world, route.track(), None, Scopes::new(None)); self.call(&mut vm, args) } @@ -220,15 +227,12 @@ impl Closure { } // Determine the route inside the closure. - let detached = vm.route.is_empty(); - let route = if detached { - self.location.into_iter().collect() - } else { - vm.route.clone() - }; + let detached = vm.location.is_none(); + let fresh = Route::new(self.location); + let route = if detached { fresh.track() } else { vm.route }; // Evaluate the body. - let mut sub = Vm::new(vm.world, route, scopes); + let mut sub = Vm::new(vm.world, route, self.location, scopes); let result = self.body.eval(&mut sub); // Handle control flow. diff --git a/src/eval/mod.rs b/src/eval/mod.rs index eeb95534c..fb65420d0 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -34,6 +34,7 @@ pub use vm::*; use std::collections::BTreeMap; +use comemo::{Track, Tracked}; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, SourceResult, StrResult, Trace, Tracepoint}; @@ -51,24 +52,24 @@ use crate::World; /// Returns either a module containing a scope with top-level bindings and /// layoutable contents or diagnostics in the form of a vector of error /// messages with file and span information. -pub fn evaluate( - world: &dyn World, +#[comemo::memoize] +pub fn eval( + world: Tracked, + route: Tracked, id: SourceId, - mut route: Vec, ) -> SourceResult { // Prevent cyclic evaluation. - if route.contains(&id) { + if route.contains(id) { let path = world.source(id).path().display(); panic!("Tried to cyclicly evaluate {}", path); } - route.push(id); - // Evaluate the module. + let route = unsafe { Route::insert(route, id) }; let ast = world.source(id).ast()?; let std = &world.config().std; let scopes = Scopes::new(Some(std)); - let mut vm = Vm::new(world, route, scopes); + let mut vm = Vm::new(world, route.track(), Some(id), scopes); let result = ast.eval(&mut vm); // Handle control flow. @@ -80,6 +81,39 @@ pub fn evaluate( Ok(Module { scope: vm.scopes.top, content: result? }) } +/// A route of source ids. +#[derive(Default)] +pub struct Route { + parent: Option>, + id: Option, +} + +impl Route { + /// Create a new, empty route. + pub fn new(id: Option) -> Self { + Self { id, parent: None } + } + + /// Insert a new id into the route. + /// + /// You must guarantee that `outer` lives longer than the resulting + /// route is ever used. + unsafe fn insert(outer: Tracked, id: SourceId) -> Route { + Route { + parent: Some(std::mem::transmute(outer)), + id: Some(id), + } + } +} + +#[comemo::track] +impl Route { + /// Whether the given id is part of the route. + fn contains(&self, id: SourceId) -> bool { + self.id == Some(id) || self.parent.map_or(false, |parent| parent.contains(id)) + } +} + /// An evaluated module, ready for importing or layouting. #[derive(Debug, Clone)] pub struct Module { @@ -696,7 +730,7 @@ impl Eval for ClosureExpr { // Define the actual function. Ok(Value::Func(Func::from_closure(Closure { - location: vm.route.last().copied(), + location: vm.location, name, captured, params, @@ -755,7 +789,7 @@ impl Eval for ShowExpr { let body = self.body(); let span = body.span(); let func = Func::from_closure(Closure { - location: vm.route.last().copied(), + location: vm.location, name: None, captured, params, @@ -940,14 +974,13 @@ fn import(vm: &mut Vm, path: &str, span: Span) -> SourceResult { let id = vm.world.resolve(&full).at(span)?; // Prevent cyclic importing. - if vm.route.contains(&id) { + if vm.route.contains(id) { bail!(span, "cyclic import"); } // Evaluate the file. - let route = vm.route.clone(); let module = - evaluate(vm.world, id, route).trace(vm.world, || Tracepoint::Import, span)?; + eval(vm.world, vm.route, id).trace(vm.world, || Tracepoint::Import, span)?; Ok(module) } diff --git a/src/eval/vm.rs b/src/eval/vm.rs index 7c8e8e31a..0604e7bef 100644 --- a/src/eval/vm.rs +++ b/src/eval/vm.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; -use super::{Scopes, Value}; +use comemo::Tracked; + +use super::{Route, Scopes, Value}; use crate::diag::{SourceError, StrResult}; use crate::source::SourceId; use crate::syntax::Span; @@ -8,27 +10,40 @@ use crate::util::PathExt; use crate::World; /// A virtual machine. -pub struct Vm<'w> { +pub struct Vm<'a> { /// The core context. - pub world: &'w dyn World, + pub world: Tracked<'a, dyn World>, /// The route of source ids the machine took to reach its current location. - pub route: Vec, + pub route: Tracked<'a, Route>, + /// The current location. + pub location: Option, /// The stack of scopes. - pub scopes: Scopes<'w>, + pub scopes: Scopes<'a>, /// A control flow event that is currently happening. pub flow: Option, } -impl<'w> Vm<'w> { +impl<'a> Vm<'a> { /// Create a new virtual machine. - pub fn new(ctx: &'w dyn World, route: Vec, scopes: Scopes<'w>) -> Self { - Self { world: ctx, route, scopes, flow: None } + pub fn new( + world: Tracked<'a, dyn World>, + route: Tracked<'a, Route>, + location: Option, + scopes: Scopes<'a>, + ) -> Self { + Self { + world, + route, + location, + scopes, + flow: None, + } } /// Resolve a user-entered path to be relative to the compilation /// environment's root. pub fn locate(&self, path: &str) -> StrResult { - if let Some(&id) = self.route.last() { + if let Some(id) = self.location { if let Some(path) = path.strip_prefix('/') { return Ok(self.world.config().root.join(path).normalize()); } diff --git a/src/font/book.rs b/src/font/book.rs index 323eb84f7..29190516f 100644 --- a/src/font/book.rs +++ b/src/font/book.rs @@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; /// Metadata about a collection of fonts. -#[derive(Default)] +#[derive(Default, Clone, Hash)] pub struct FontBook { /// Maps from lowercased family names to font indices. families: BTreeMap>, @@ -144,7 +144,7 @@ impl FontBook { } /// Properties of a single font. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct FontInfo { /// The typographic font family this font is part of. pub family: String, @@ -377,7 +377,7 @@ fn shared_prefix_words(left: &str, right: &str) -> usize { /// - 2 codepoints inside (18, 19) /// /// So the resulting encoding is `[2, 3, 4, 3, 3, 1, 2, 2]`. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Coverage(Vec); diff --git a/src/lib.rs b/src/lib.rs index 6122ffeda..e288d556a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ //! [parsed]: parse::parse //! [syntax tree]: syntax::SyntaxNode //! [AST]: syntax::ast -//! [evaluate]: eval::evaluate +//! [evaluate]: eval::eval //! [module]: eval::Module //! [content]: model::Content //! [layouted]: model::layout @@ -51,8 +51,10 @@ pub mod syntax; use std::path::{Path, PathBuf}; +use comemo::{Prehashed, Track}; + use crate::diag::{FileResult, SourceResult}; -use crate::eval::Scope; +use crate::eval::{Route, Scope}; use crate::font::{Font, FontBook}; use crate::frame::Frame; use crate::model::StyleMap; @@ -64,33 +66,39 @@ use crate::util::Buffer; /// Returns either a vector of frames representing individual pages or /// diagnostics in the form of a vector of error message with file and span /// information. -pub fn typeset(world: &dyn World, main: SourceId) -> SourceResult> { - let module = eval::evaluate(world, main, vec![])?; - model::layout(world, &module.content) +pub fn typeset( + world: &(dyn World + 'static), + main: SourceId, +) -> SourceResult> { + let route = Route::default(); + let module = eval::eval(world.track(), route.track(), main)?; + model::layout(world.track(), &module.content) } /// The environment in which typesetting occurs. +#[comemo::track] pub trait World { /// Access the global configuration. - fn config(&self) -> &Config; - - /// Try to resolve the unique id of a source file. - fn resolve(&self, path: &Path) -> FileResult; - - /// Access a source file by id. - fn source(&self, id: SourceId) -> &Source; + fn config(&self) -> &Prehashed; /// Metadata about all known fonts. - fn book(&self) -> &FontBook; + fn book(&self) -> &Prehashed; /// Try to access the font with the given id. fn font(&self, id: usize) -> Option; /// Try to access a file at a path. fn file(&self, path: &Path) -> FileResult; + + /// Try to resolve the unique id of a source file. + fn resolve(&self, path: &Path) -> FileResult; + + /// Access a source file by id. + fn source(&self, id: SourceId) -> &Source; } /// The global configuration for typesetting. +#[derive(Debug, Clone, Hash)] pub struct Config { /// The compilation root, relative to which absolute paths are. /// diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs index 505dd1f67..656842722 100644 --- a/src/library/graphics/hide.rs +++ b/src/library/graphics/hide.rs @@ -14,7 +14,7 @@ impl HideNode { impl Layout for HideNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs index c0249b3cb..7523471da 100644 --- a/src/library/graphics/image.rs +++ b/src/library/graphics/image.rs @@ -41,7 +41,7 @@ impl ImageNode { impl Layout for ImageNode { fn layout( &self, - _: &dyn World, + _: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs index ebfec1b29..192f83507 100644 --- a/src/library/graphics/line.rs +++ b/src/library/graphics/line.rs @@ -40,7 +40,7 @@ impl LineNode { impl Layout for LineNode { fn layout( &self, - _: &dyn World, + _: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index d91625572..eea025686 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -78,7 +78,7 @@ impl ShapeNode { impl Layout for ShapeNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs index 34d45bd0c..b110f343a 100644 --- a/src/library/graphics/transform.rs +++ b/src/library/graphics/transform.rs @@ -25,7 +25,7 @@ impl MoveNode { impl Layout for MoveNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -86,7 +86,7 @@ impl TransformNode { impl Layout for TransformNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs index 0c758cf2a..705d555bd 100644 --- a/src/library/layout/align.rs +++ b/src/library/layout/align.rs @@ -28,7 +28,7 @@ impl AlignNode { impl Layout for AlignNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs index e0163f633..8ae4394e4 100644 --- a/src/library/layout/columns.rs +++ b/src/library/layout/columns.rs @@ -28,7 +28,7 @@ impl ColumnsNode { impl Layout for ColumnsNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index 05c10789c..7cb529107 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -25,7 +25,7 @@ pub enum FlowChild { impl Layout for FlowNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -149,7 +149,7 @@ impl FlowLayouter { /// Layout a node. pub fn layout_node( &mut self, - world: &dyn World, + world: Tracked, node: &LayoutNode, styles: StyleChain, ) -> SourceResult<()> { diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index cd4fc6b4c..2c246df98 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -33,7 +33,7 @@ impl GridNode { impl Layout for GridNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -93,7 +93,7 @@ castable! { /// Performs grid layout. pub struct GridLayouter<'a> { /// The core context. - world: &'a dyn World, + world: Tracked<'a, dyn World>, /// The grid cells. cells: &'a [LayoutNode], /// The column tracks including gutter tracks. @@ -133,7 +133,7 @@ impl<'a> GridLayouter<'a> { /// /// This prepares grid layout by unifying content and gutter tracks. pub fn new( - world: &'a dyn World, + world: Tracked<'a, dyn World>, tracks: Spec<&[TrackSizing]>, gutter: Spec<&[TrackSizing]>, cells: &'a [LayoutNode], diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs index 983bfa111..06c3672f2 100644 --- a/src/library/layout/pad.rs +++ b/src/library/layout/pad.rs @@ -28,7 +28,7 @@ impl PadNode { impl Layout for PadNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index ba5972630..9cbbcca5f 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -57,7 +57,7 @@ impl PageNode { /// Layout the page run into a sequence of frames, one per page. pub fn layout( &self, - world: &dyn World, + world: Tracked, mut page: usize, styles: StyleChain, ) -> SourceResult> { @@ -180,7 +180,7 @@ impl Marginal { /// Resolve the marginal based on the page number. pub fn resolve( &self, - world: &dyn World, + world: Tracked, page: usize, ) -> SourceResult> { Ok(match self { diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs index 862c969e0..01da62e5c 100644 --- a/src/library/layout/place.rs +++ b/src/library/layout/place.rs @@ -21,7 +21,7 @@ impl PlaceNode { impl Layout for PlaceNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs index a9fc16212..b9a26642b 100644 --- a/src/library/layout/stack.rs +++ b/src/library/layout/stack.rs @@ -27,7 +27,7 @@ impl StackNode { impl Layout for StackNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -168,7 +168,7 @@ impl<'a> StackLayouter<'a> { /// Layout an arbitrary node. pub fn layout_node( &mut self, - world: &dyn World, + world: Tracked, node: &LayoutNode, styles: StyleChain, ) -> SourceResult<()> { diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs index d71f69764..1f5ea8f37 100644 --- a/src/library/math/mod.rs +++ b/src/library/math/mod.rs @@ -48,7 +48,11 @@ impl Show for MathNode { } } - fn realize(&self, _: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + _: Tracked, + styles: StyleChain, + ) -> SourceResult { let node = self::rex::RexNode { tex: self.formula.clone(), display: self.display, @@ -64,7 +68,7 @@ impl Show for MathNode { fn finalize( &self, - _: &dyn World, + _: Tracked, styles: StyleChain, mut realized: Content, ) -> SourceResult { diff --git a/src/library/math/rex.rs b/src/library/math/rex.rs index 76ba5177b..96e8e4380 100644 --- a/src/library/math/rex.rs +++ b/src/library/math/rex.rs @@ -22,7 +22,7 @@ pub struct RexNode { impl Layout for RexNode { fn layout( &self, - world: &dyn World, + world: Tracked, _: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/prelude.rs b/src/library/prelude.rs index 48eebaf60..44d1af7ff 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -6,6 +6,7 @@ pub use std::io; pub use std::num::NonZeroUsize; pub use std::sync::Arc; +pub use comemo::Tracked; pub use typst_macros::node; pub use crate::diag::{with_alternative, At, SourceError, SourceResult, StrResult}; diff --git a/src/library/structure/doc.rs b/src/library/structure/doc.rs index ba848b64c..c3af3f1ca 100644 --- a/src/library/structure/doc.rs +++ b/src/library/structure/doc.rs @@ -9,7 +9,7 @@ impl DocNode { /// Layout the document into a sequence of frames, one per page. pub fn layout( &self, - world: &dyn World, + world: Tracked, styles: StyleChain, ) -> SourceResult> { let mut frames = vec![]; diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index 855c0503c..017384960 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -82,13 +82,13 @@ impl Show for HeadingNode { } } - fn realize(&self, _: &dyn World, _: StyleChain) -> SourceResult { + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { Ok(Content::block(self.body.clone())) } fn finalize( &self, - world: &dyn World, + world: Tracked, styles: StyleChain, mut realized: Content, ) -> SourceResult { @@ -149,7 +149,11 @@ pub enum Leveled { impl Leveled { /// Resolve the value based on the level. - pub fn resolve(&self, world: &dyn World, level: NonZeroUsize) -> SourceResult { + pub fn resolve( + &self, + world: Tracked, + level: NonZeroUsize, + ) -> SourceResult { Ok(match self { Self::Value(value) => value.clone(), Self::Mapping(mapping) => mapping(level), diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index f63374f3e..9da147338 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -100,7 +100,11 @@ impl Show for ListNode { } } - fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { let mut cells = vec![]; let mut number = self.start; @@ -145,7 +149,7 @@ impl Show for ListNode { fn finalize( &self, - _: &dyn World, + _: Tracked, styles: StyleChain, realized: Content, ) -> SourceResult { @@ -208,7 +212,7 @@ impl Label { /// Resolve the value based on the level. pub fn resolve( &self, - world: &dyn World, + world: Tracked, kind: ListKind, number: usize, ) -> SourceResult { diff --git a/src/library/structure/reference.rs b/src/library/structure/reference.rs index 5d1dab38d..0a9f4f9ce 100644 --- a/src/library/structure/reference.rs +++ b/src/library/structure/reference.rs @@ -22,7 +22,7 @@ impl Show for RefNode { } } - fn realize(&self, _: &dyn World, _: StyleChain) -> SourceResult { + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { Ok(Content::Text(format_eco!("@{}", self.0))) } } diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index f1ca7e039..9f89cd2bd 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -72,7 +72,11 @@ impl Show for TableNode { } } - fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { let fill = styles.get(Self::FILL); let stroke = styles.get(Self::STROKE).map(RawStroke::unwrap_or_default); let padding = styles.get(Self::PADDING); @@ -110,7 +114,7 @@ impl Show for TableNode { fn finalize( &self, - _: &dyn World, + _: Tracked, styles: StyleChain, realized: Content, ) -> SourceResult { @@ -129,7 +133,12 @@ pub enum Celled { impl Celled { /// Resolve the value based on the cell position. - pub fn resolve(&self, world: &dyn World, x: usize, y: usize) -> SourceResult { + pub fn resolve( + &self, + world: Tracked, + x: usize, + y: usize, + ) -> SourceResult { Ok(match self { Self::Value(value) => value.clone(), Self::Func(func, span) => { diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index 3d030d457..1242488bf 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -48,7 +48,11 @@ impl Show for DecoNode { dict! { "body" => Value::Content(self.0.clone()) } } - fn realize(&self, _: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + _: Tracked, + styles: StyleChain, + ) -> SourceResult { Ok(self.0.clone().styled(TextNode::DECO, Decoration { line: L, stroke: styles.get(Self::STROKE).unwrap_or_default(), diff --git a/src/library/text/link.rs b/src/library/text/link.rs index f89bbd674..c06fea55d 100644 --- a/src/library/text/link.rs +++ b/src/library/text/link.rs @@ -64,7 +64,7 @@ impl Show for LinkNode { } } - fn realize(&self, _: &dyn World, _: StyleChain) -> SourceResult { + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { Ok(self.body.clone().unwrap_or_else(|| match &self.dest { Destination::Url(url) => { let mut text = url.as_str(); @@ -80,7 +80,7 @@ impl Show for LinkNode { fn finalize( &self, - _: &dyn World, + _: Tracked, styles: StyleChain, mut realized: Content, ) -> SourceResult { diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 55b866cb6..934f5e15d 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -507,7 +507,7 @@ impl Show for StrongNode { dict! { "body" => Value::Content(self.0.clone()) } } - fn realize(&self, _: &dyn World, _: StyleChain) -> SourceResult { + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { Ok(self.0.clone().styled(TextNode::BOLD, Toggle)) } } @@ -532,7 +532,7 @@ impl Show for EmphNode { dict! { "body" => Value::Content(self.0.clone()) } } - fn realize(&self, _: &dyn World, _: StyleChain) -> SourceResult { + fn realize(&self, _: Tracked, _: StyleChain) -> SourceResult { Ok(self.0.clone().styled(TextNode::ITALIC, Toggle)) } } diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 00a1e0346..6910c23ac 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -64,7 +64,7 @@ impl ParNode { impl Layout for ParNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -496,7 +496,7 @@ fn collect<'a>( /// Prepare paragraph layout by shaping the whole paragraph and layouting all /// contained inline-level nodes. fn prepare<'a>( - world: &dyn World, + world: Tracked, par: &'a ParNode, text: &'a str, segments: Vec<(Segment<'a>, StyleChain<'a>)>, @@ -561,7 +561,7 @@ fn prepare<'a>( /// items for them. fn shape_range<'a>( items: &mut Vec>, - world: &dyn World, + world: Tracked, bidi: &BidiInfo<'a>, range: Range, styles: StyleChain<'a>, @@ -627,7 +627,7 @@ fn shared_get<'a, K: Key<'a>>( /// Find suitable linebreaks. fn linebreak<'a>( p: &'a Preparation<'a>, - world: &dyn World, + world: Tracked, width: Length, ) -> Vec> { match p.styles.get(ParNode::LINEBREAKS) { @@ -641,7 +641,7 @@ fn linebreak<'a>( /// very unbalanced line, but is fast and simple. fn linebreak_simple<'a>( p: &'a Preparation<'a>, - world: &dyn World, + world: Tracked, width: Length, ) -> Vec> { let mut lines = vec![]; @@ -701,7 +701,7 @@ fn linebreak_simple<'a>( /// text. fn linebreak_optimized<'a>( p: &'a Preparation<'a>, - world: &dyn World, + world: Tracked, width: Length, ) -> Vec> { /// The cost of a line or paragraph layout. @@ -914,7 +914,7 @@ impl Breakpoints<'_> { /// Create a line which spans the given range. fn line<'a>( p: &'a Preparation, - world: &dyn World, + world: Tracked, mut range: Range, mandatory: bool, hyphen: bool, @@ -1022,7 +1022,7 @@ fn line<'a>( /// Combine layouted lines into one frame per region. fn stack( p: &Preparation, - world: &dyn World, + world: Tracked, lines: &[Line], regions: &Regions, ) -> SourceResult> { @@ -1072,7 +1072,7 @@ fn stack( /// Commit to a line and build its frame. fn commit( p: &Preparation, - world: &dyn World, + world: Tracked, line: &Line, regions: &Regions, width: Length, diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index 5bce2a90f..e7c73a913 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -59,7 +59,11 @@ impl Show for RawNode { } } - fn realize(&self, _: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + _: Tracked, + styles: StyleChain, + ) -> SourceResult { let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase()); let foreground = THEME .settings @@ -111,7 +115,7 @@ impl Show for RawNode { fn finalize( &self, - _: &dyn World, + _: Tracked, styles: StyleChain, mut realized: Content, ) -> SourceResult { diff --git a/src/library/text/repeat.rs b/src/library/text/repeat.rs index 78a210694..e3bae3fcb 100644 --- a/src/library/text/repeat.rs +++ b/src/library/text/repeat.rs @@ -14,7 +14,7 @@ impl RepeatNode { impl Layout for RepeatNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index c1d0341b9..16989acf3 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -80,7 +80,7 @@ impl<'a> ShapedText<'a> { /// /// The `justification` defines how much extra advance width each /// [justifiable glyph](ShapedGlyph::is_justifiable) will get. - pub fn build(&self, world: &dyn World, justification: Length) -> Frame { + pub fn build(&self, world: Tracked, justification: Length) -> Frame { let (top, bottom) = self.measure(world); let size = Size::new(self.width, top + bottom); @@ -144,7 +144,7 @@ impl<'a> ShapedText<'a> { } /// Measure the top and bottom extent of this text. - fn measure(&self, world: &dyn World) -> (Length, Length) { + fn measure(&self, world: Tracked) -> (Length, Length) { let mut top = Length::zero(); let mut bottom = Length::zero(); @@ -199,7 +199,7 @@ impl<'a> ShapedText<'a> { /// shaping process if possible. pub fn reshape( &'a self, - world: &dyn World, + world: Tracked, text_range: Range, ) -> ShapedText<'a> { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { @@ -218,7 +218,7 @@ impl<'a> ShapedText<'a> { } /// Push a hyphen to end of the text. - pub fn push_hyphen(&mut self, world: &dyn World) { + pub fn push_hyphen(&mut self, world: Tracked) { families(self.styles).find_map(|family| { let font = world .book() @@ -306,7 +306,7 @@ impl Debug for ShapedText<'_> { /// Holds shaping results and metadata common to all shaped segments. struct ShapingContext<'a> { - world: &'a dyn World, + world: Tracked<'a, dyn World>, glyphs: Vec, used: Vec, styles: StyleChain<'a>, @@ -319,7 +319,7 @@ struct ShapingContext<'a> { /// Shape text into [`ShapedText`]. pub fn shape<'a>( - world: &dyn World, + world: Tracked, text: &'a str, styles: StyleChain<'a>, dir: Dir, diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs index b359c5ed0..6a5415e8a 100644 --- a/src/library/text/shift.rs +++ b/src/library/text/shift.rs @@ -42,7 +42,11 @@ impl Show for ShiftNode { dict! { "body" => Value::Content(self.0.clone()) } } - fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { let mut transformed = None; if styles.get(Self::TYPOGRAPHIC) { if let Some(text) = search_text(&self.0, S) { @@ -91,7 +95,7 @@ fn search_text(content: &Content, mode: ScriptKind) -> Option { /// Checks whether the first retrievable family contains all code points of the /// given string. -fn is_shapable(world: &dyn World, text: &str, styles: StyleChain) -> bool { +fn is_shapable(world: Tracked, text: &str, styles: StyleChain) -> bool { for family in styles.get(TextNode::FAMILY).iter() { if let Some(font) = world .book() diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 3fc413f76..d9b19d64f 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -10,7 +10,9 @@ pub use data::*; pub use math::*; pub use string::*; -use crate::eval::{Eval, Scopes, Vm}; +use comemo::Track; + +use crate::eval::{Eval, Route, Scopes, Vm}; use crate::library::prelude::*; use crate::source::Source; @@ -39,7 +41,8 @@ pub fn eval(vm: &mut Vm, args: &mut Args) -> SourceResult { // Evaluate the source. let std = &vm.world.config().std; let scopes = Scopes::new(Some(std)); - let mut sub = Vm::new(vm.world, vec![], scopes); + let route = Route::default(); + let mut sub = Vm::new(vm.world, route.track(), None, scopes); let result = ast.eval(&mut sub); // Handle control flow. diff --git a/src/main.rs b/src/main.rs index 9a6e367f9..1c5790c21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,17 @@ -use std::cell::RefCell; -use std::collections::{hash_map::Entry, HashMap}; +use std::cell::{RefCell, RefMut}; +use std::collections::HashMap; use std::fs::{self, File}; use std::hash::Hash; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use std::process; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::{self, termcolor}; +use comemo::Prehashed; use elsa::FrozenVec; use memmap2::Mmap; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use once_cell::unsync::OnceCell; use pico_args::Arguments; use same_file::{is_same_file, Handle}; @@ -19,10 +21,8 @@ use walkdir::WalkDir; use typst::diag::{FileError, FileResult, SourceError, StrResult}; use typst::font::{Font, FontBook, FontInfo, FontVariant}; -use typst::library::text::THEME; -use typst::parse::TokenMode; use typst::source::{Source, SourceId}; -use typst::util::Buffer; +use typst::util::{Buffer, PathExt}; use typst::{Config, World}; type CodespanResult = Result; @@ -31,7 +31,6 @@ type CodespanError = codespan_reporting::files::Error; /// What to do. enum Command { Typeset(TypesetCommand), - Highlight(HighlightCommand), Fonts(FontsCommand), } @@ -40,6 +39,7 @@ struct TypesetCommand { input: PathBuf, output: PathBuf, root: Option, + watch: bool, } const HELP: &'static str = "\ @@ -55,33 +55,13 @@ ARGS: OPTIONS: -h, --help Print this help + -w, --watch Watch the inputs and recompile on changes --root Configure the root for absolute paths SUBCOMMANDS: - --highlight Highlight .typ files to HTML --fonts List all discovered system fonts "; -/// Highlight a .typ file into an HTML file. -struct HighlightCommand { - input: PathBuf, - output: PathBuf, -} - -const HELP_HIGHLIGHT: &'static str = "\ -typst --highlight creates highlighted HTML from .typ files - -USAGE: - typst --highlight [OPTIONS] [output.html] - -ARGS: - Path to input Typst file - [output.html] Path to output HTML file - -OPTIONS: - -h, --help Print this help -"; - /// List discovered system fonts. struct FontsCommand { variants: bool, @@ -116,14 +96,7 @@ fn parse_args() -> StrResult { let mut args = Arguments::from_env(); let help = args.contains(["-h", "--help"]); - let command = if args.contains("--highlight") { - if help { - print_help(HELP_HIGHLIGHT); - } - - let (input, output) = parse_input_output(&mut args, "html")?; - Command::Highlight(HighlightCommand { input, output }) - } else if args.contains("--fonts") { + let command = if args.contains("--fonts") { if help { print_help(HELP_FONTS); } @@ -135,8 +108,9 @@ fn parse_args() -> StrResult { } let root = args.opt_value_from_str("--root").map_err(|_| "missing root path")?; + let watch = args.contains(["-w", "--watch"]); let (input, output) = parse_input_output(&mut args, "pdf")?; - Command::Typeset(TypesetCommand { input, output, root }) + Command::Typeset(TypesetCommand { input, output, watch, root }) }; // Don't allow excess arguments. @@ -194,33 +168,84 @@ fn print_error(msg: &str) -> io::Result<()> { fn dispatch(command: Command) -> StrResult<()> { match command { Command::Typeset(command) => typeset(command), - Command::Highlight(command) => highlight(command), Command::Fonts(command) => fonts(command), } } /// Execute a typesetting command. fn typeset(command: TypesetCommand) -> StrResult<()> { - let mut world = SystemWorld::new(); + let mut config = Config::default(); if let Some(root) = &command.root { - world.config.root = root.clone(); + config.root = root.clone(); } else if let Some(dir) = command.input.parent() { - world.config.root = dir.into(); + config.root = dir.into(); } + // Create the world that serves sources, fonts and files. - let id = world.resolve(&command.input).map_err(|err| err.to_string())?; + let mut world = SystemWorld::new(config); // Typeset. - match typst::typeset(&world, id) { + typeset_once(&mut world, &command)?; + + if !command.watch { + return Ok(()); + } + + // Setup file watching. + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()) + .map_err(|_| "failed to watch directory")?; + + // Watch this directory recursively. + watcher + .watch(Path::new("."), RecursiveMode::Recursive) + .map_err(|_| "failed to watch directory")?; + + // Handle events. + let timeout = std::time::Duration::from_millis(100); + loop { + let mut recompile = false; + for event in rx + .recv() + .into_iter() + .chain(std::iter::from_fn(|| rx.recv_timeout(timeout).ok())) + { + let event = event.map_err(|_| "failed to watch directory")?; + if event + .paths + .iter() + .all(|path| is_same_file(path, &command.output).unwrap_or(false)) + { + continue; + } + + recompile |= world.relevant(&event); + } + + if recompile { + typeset_once(&mut world, &command)?; + } + } +} + +/// Typeset a single time. +fn typeset_once(world: &mut SystemWorld, command: &TypesetCommand) -> StrResult<()> { + status(command, Status::Compiling).unwrap(); + + world.reset(); + let main = world.resolve(&command.input).map_err(|err| err.to_string())?; + match typst::typeset(world, main) { // Export the PDF. Ok(frames) => { let buffer = typst::export::pdf(&frames); fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; + status(command, Status::Success).unwrap(); } // Print diagnostics. Err(errors) => { + status(command, Status::Error).unwrap(); print_diagnostics(&world, *errors) .map_err(|_| "failed to print diagnostics")?; } @@ -229,6 +254,65 @@ fn typeset(command: TypesetCommand) -> StrResult<()> { Ok(()) } +/// Clear the terminal and render the status message. +fn status(command: &TypesetCommand, status: Status) -> io::Result<()> { + if !command.watch { + return Ok(()); + } + + let esc = 27 as char; + let input = command.input.display(); + let output = command.output.display(); + let time = chrono::offset::Local::now(); + let timestamp = time.format("%H:%M:%S"); + let message = status.message(); + let color = status.color(); + + let mut w = StandardStream::stderr(ColorChoice::Always); + write!(w, "{esc}c{esc}[1;1H")?; + + w.set_color(&color)?; + write!(w, "watching")?; + w.reset()?; + writeln!(w, " {input}")?; + + w.set_color(&color)?; + write!(w, "writing to")?; + w.reset()?; + writeln!(w, " {output}")?; + + writeln!(w)?; + writeln!(w, "[{timestamp}] {message}")?; + writeln!(w)?; + + w.flush() +} + +/// The status in which the watcher can be. +enum Status { + Compiling, + Success, + Error, +} + +impl Status { + fn message(&self) -> &str { + match self { + Self::Compiling => "compiling ...", + Self::Success => "compiled successfully", + Self::Error => "compiled with errors", + } + } + + fn color(&self) -> termcolor::ColorSpec { + let styles = term::Styles::default(); + match self { + Self::Error => styles.header_error, + _ => styles.header_note, + } + } +} + /// Print diagnostic messages to the terminal. fn print_diagnostics( world: &SystemWorld, @@ -263,21 +347,11 @@ fn print_diagnostics( Ok(()) } -/// Execute a highlighting command. -fn highlight(command: HighlightCommand) -> StrResult<()> { - let input = - fs::read_to_string(&command.input).map_err(|_| "failed to load source file")?; - - let html = typst::syntax::highlight_html(&input, TokenMode::Markup, &THEME); - fs::write(&command.output, html).map_err(|_| "failed to write HTML file")?; - - Ok(()) -} - /// Execute a font listing command. fn fonts(command: FontsCommand) -> StrResult<()> { - let world = SystemWorld::new(); - for (name, infos) in world.book().families() { + let mut searcher = FontSearcher::new(); + searcher.search_system(); + for (name, infos) in searcher.book.families() { println!("{name}"); if command.variants { for info in infos { @@ -292,60 +366,50 @@ fn fonts(command: FontsCommand) -> StrResult<()> { /// A world that provides access to the operating system. struct SystemWorld { - config: Config, - sources: FrozenVec>, - nav: RefCell>, - book: FontBook, + config: Prehashed, + book: Prehashed, fonts: Vec, - files: RefCell>, + hashes: RefCell>>, + paths: RefCell>, + sources: FrozenVec>, } +/// Holds details about the location of a font and lazily the font itself. struct FontSlot { path: PathBuf, index: u32, font: OnceCell>, } +/// Holds canonical data for all paths pointing to the same entity. +#[derive(Default)] +struct PathSlot { + source: OnceCell>, + buffer: OnceCell>, +} + impl SystemWorld { - fn new() -> Self { - let mut world = Self { - config: Config::default(), - book: FontBook::new(), + fn new(config: Config) -> Self { + let mut searcher = FontSearcher::new(); + searcher.search_system(); + + Self { + config: Prehashed::new(config), + book: Prehashed::new(searcher.book), + fonts: searcher.fonts, + hashes: RefCell::default(), + paths: RefCell::default(), sources: FrozenVec::new(), - nav: RefCell::new(HashMap::new()), - fonts: vec![], - files: RefCell::new(HashMap::new()), - }; - world.search_system(); - world + } } } impl World for SystemWorld { - fn config(&self) -> &Config { + fn config(&self) -> &Prehashed { &self.config } - fn resolve(&self, path: &Path) -> FileResult { - let hash = PathHash::new(path)?; - if let Some(&id) = self.nav.borrow().get(&hash) { - return Ok(id); - } - - let text = fs::read_to_string(path).map_err(|e| FileError::from_io(e, path))?; - let id = SourceId::from_raw(self.sources.len() as u16); - let source = Source::new(id, path, text); - self.sources.push(Box::new(source)); - self.nav.borrow_mut().insert(hash, id); - - Ok(id) - } - - fn source(&self, id: SourceId) -> &Source { - &self.sources[id.into_raw() as usize] - } - - fn book(&self) -> &FontBook { + fn book(&self) -> &Prehashed { &self.book } @@ -360,36 +424,178 @@ impl World for SystemWorld { } fn file(&self, path: &Path) -> FileResult { - let hash = PathHash::new(path)?; - Ok(match self.files.borrow_mut().entry(hash) { - Entry::Occupied(entry) => entry.get().clone(), - Entry::Vacant(entry) => entry - .insert(fs::read(path).map_err(|e| FileError::from_io(e, path))?.into()) - .clone(), - }) + self.slot(path)? + .buffer + .get_or_init(|| read(path).map(Buffer::from)) + .clone() + } + + fn resolve(&self, path: &Path) -> FileResult { + self.slot(path)? + .source + .get_or_init(|| { + let buf = read(path)?; + let text = String::from_utf8(buf)?; + Ok(self.insert(path, text)) + }) + .clone() + } + + fn source(&self, id: SourceId) -> &Source { + &self.sources[id.into_u16() as usize] } } -/// A hash that is the same for all paths pointing to the same file. +impl SystemWorld { + fn slot(&self, path: &Path) -> FileResult> { + let mut hashes = self.hashes.borrow_mut(); + let hash = match hashes.get(path).cloned() { + Some(hash) => hash, + None => { + let hash = PathHash::new(path); + if let Ok(canon) = path.canonicalize() { + hashes.insert(canon.normalize(), hash.clone()); + } + hashes.insert(path.into(), hash.clone()); + hash + } + }?; + + Ok(std::cell::RefMut::map(self.paths.borrow_mut(), |paths| { + paths.entry(hash).or_default() + })) + } + + fn insert(&self, path: &Path, text: String) -> SourceId { + let id = SourceId::from_u16(self.sources.len() as u16); + let source = Source::new(id, path, text); + self.sources.push(Box::new(source)); + id + } + + fn relevant(&mut self, event: ¬ify::Event) -> bool { + match &event.kind { + notify::EventKind::Any => {} + notify::EventKind::Access(_) => return false, + notify::EventKind::Create(_) => return true, + notify::EventKind::Modify(kind) => match kind { + notify::event::ModifyKind::Any => {} + notify::event::ModifyKind::Data(_) => {} + notify::event::ModifyKind::Metadata(_) => return false, + notify::event::ModifyKind::Name(_) => return true, + notify::event::ModifyKind::Other => return false, + }, + notify::EventKind::Remove(_) => {} + notify::EventKind::Other => return false, + } + + event.paths.iter().any(|path| self.dependant(path)) + } + + fn dependant(&self, path: &Path) -> bool { + self.hashes.borrow().contains_key(&path.normalize()) + || PathHash::new(path) + .map_or(false, |hash| self.paths.borrow().contains_key(&hash)) + } + + fn reset(&mut self) { + self.sources.as_mut().clear(); + self.hashes.borrow_mut().clear(); + self.paths.borrow_mut().clear(); + } +} + +/// A hash that is the same for all paths pointing to the same entity. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] struct PathHash(u128); impl PathHash { fn new(path: &Path) -> FileResult { let f = |e| FileError::from_io(e, path); - let file = File::open(path).map_err(f)?; - if file.metadata().map_err(f)?.is_file() { - let handle = Handle::from_file(file).map_err(f)?; - let mut state = SipHasher::new(); - handle.hash(&mut state); - Ok(Self(state.finish128().as_u128())) - } else { - Err(FileError::NotFound(path.into())) - } + let handle = Handle::from_path(path).map_err(f)?; + let mut state = SipHasher::new(); + handle.hash(&mut state); + Ok(Self(state.finish128().as_u128())) } } -impl SystemWorld { +/// Read a file. +fn read(path: &Path) -> FileResult> { + let f = |e| FileError::from_io(e, path); + let mut file = File::open(path).map_err(f)?; + if file.metadata().map_err(f)?.is_file() { + let mut data = vec![]; + file.read_to_end(&mut data).map_err(f)?; + Ok(data) + } else { + Err(FileError::IsDirectory) + } +} + +impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { + type FileId = SourceId; + type Name = std::path::Display<'a>; + type Source = &'a str; + + fn name(&'a self, id: SourceId) -> CodespanResult { + Ok(World::source(self, id).path().display()) + } + + fn source(&'a self, id: SourceId) -> CodespanResult { + Ok(World::source(self, id).text()) + } + + fn line_index(&'a self, id: SourceId, given: usize) -> CodespanResult { + let source = World::source(self, id); + source + .byte_to_line(given) + .ok_or_else(|| CodespanError::IndexTooLarge { + given, + max: source.len_bytes(), + }) + } + + fn line_range( + &'a self, + id: SourceId, + given: usize, + ) -> CodespanResult> { + let source = World::source(self, id); + source + .line_to_range(given) + .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() }) + } + + fn column_number( + &'a self, + id: SourceId, + _: usize, + given: usize, + ) -> CodespanResult { + let source = World::source(self, id); + source.byte_to_column(given).ok_or_else(|| { + let max = source.len_bytes(); + if given <= max { + CodespanError::InvalidCharBoundary { given } + } else { + CodespanError::IndexTooLarge { given, max } + } + }) + } +} + +/// Searches for fonts. +struct FontSearcher { + book: FontBook, + fonts: Vec, +} + +impl FontSearcher { + /// Create a new, empty system searcher. + fn new() -> Self { + Self { book: FontBook::new(), fonts: vec![] } + } + /// Search for fonts in the linux system font directories. #[cfg(all(unix, not(target_os = "macos")))] fn search_system(&mut self) { @@ -466,55 +672,3 @@ impl SystemWorld { } } } - -impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { - type FileId = SourceId; - type Name = std::path::Display<'a>; - type Source = &'a str; - - fn name(&'a self, id: SourceId) -> CodespanResult { - Ok(World::source(self, id).path().display()) - } - - fn source(&'a self, id: SourceId) -> CodespanResult { - Ok(World::source(self, id).text()) - } - - fn line_index(&'a self, id: SourceId, given: usize) -> CodespanResult { - let source = World::source(self, id); - source - .byte_to_line(given) - .ok_or_else(|| CodespanError::IndexTooLarge { - given, - max: source.len_bytes(), - }) - } - - fn line_range( - &'a self, - id: SourceId, - given: usize, - ) -> CodespanResult> { - let source = World::source(self, id); - source - .line_to_range(given) - .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() }) - } - - fn column_number( - &'a self, - id: SourceId, - _: usize, - given: usize, - ) -> CodespanResult { - let source = World::source(self, id); - source.byte_to_column(given).ok_or_else(|| { - let max = source.len_bytes(); - if given <= max { - CodespanError::InvalidCharBoundary { given } - } else { - CodespanError::IndexTooLarge { given, max } - } - }) - } -} diff --git a/src/model/content.rs b/src/model/content.rs index dbea141cc..92d592a6a 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -4,6 +4,7 @@ use std::iter::Sum; use std::mem; use std::ops::{Add, AddAssign}; +use comemo::Tracked; use typed_arena::Arena; use super::{ @@ -23,7 +24,8 @@ use crate::World; /// Layout content into a collection of pages. /// /// Relayouts until all pinned locations are converged. -pub fn layout(world: &dyn World, content: &Content) -> SourceResult> { +#[comemo::memoize] +pub fn layout(world: Tracked, content: &Content) -> SourceResult> { let styles = StyleChain::with_root(&world.config().styles); let scratch = Scratch::default(); @@ -232,7 +234,7 @@ impl Content { impl Layout for Content { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -330,9 +332,9 @@ impl Sum for Content { } /// Builds a document or a flow node from content. -struct Builder<'a, 'w> { +struct Builder<'a> { /// The core context. - world: &'w dyn World, + world: Tracked<'a, dyn World>, /// Scratch arenas for building. scratch: &'a Scratch<'a>, /// The current document building state. @@ -354,8 +356,8 @@ struct Scratch<'a> { templates: Arena, } -impl<'a, 'w> Builder<'a, 'w> { - fn new(world: &'w dyn World, scratch: &'a Scratch<'a>, top: bool) -> Self { +impl<'a> Builder<'a> { + fn new(world: Tracked<'a, dyn World>, scratch: &'a Scratch<'a>, top: bool) -> Self { Self { world, scratch, @@ -662,7 +664,7 @@ impl<'a> ParBuilder<'a> { true } - fn finish(self, parent: &mut Builder<'a, '_>) { + fn finish(self, parent: &mut Builder<'a>) { let (mut children, shared) = self.0.finish(); if children.is_empty() { return; @@ -746,7 +748,7 @@ impl<'a> ListBuilder<'a> { true } - fn finish(self, parent: &mut Builder<'a, '_>) -> SourceResult<()> { + fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> { let (items, shared) = self.items.finish(); let kind = match items.items().next() { Some(item) => item.kind, diff --git a/src/model/layout.rs b/src/model/layout.rs index 68847471d..8064afffe 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -5,6 +5,8 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; +use comemo::{Prehashed, Tracked}; + use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry}; use crate::diag::SourceResult; use crate::eval::{RawAlign, RawLength}; @@ -14,7 +16,6 @@ use crate::geom::{ }; use crate::library::graphics::MoveNode; use crate::library::layout::{AlignNode, PadNode}; -use crate::util::Prehashed; use crate::World; /// A node that can be layouted into a sequence of regions. @@ -24,7 +25,7 @@ pub trait Layout: 'static { /// Layout this node into the given regions, producing frames. fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult>; @@ -214,9 +215,10 @@ impl LayoutNode { } impl Layout for LayoutNode { + #[comemo::memoize] fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -285,7 +287,7 @@ struct EmptyNode; impl Layout for EmptyNode { fn layout( &self, - _: &dyn World, + _: Tracked, regions: &Regions, _: StyleChain, ) -> SourceResult> { @@ -307,7 +309,7 @@ struct SizedNode { impl Layout for SizedNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -354,7 +356,7 @@ struct FillNode { impl Layout for FillNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { @@ -379,7 +381,7 @@ struct StrokeNode { impl Layout for StrokeNode { fn layout( &self, - world: &dyn World, + world: Tracked, regions: &Regions, styles: StyleChain, ) -> SourceResult> { diff --git a/src/model/property.rs b/src/model/property.rs index 8681da7d5..18f41eee5 100644 --- a/src/model/property.rs +++ b/src/model/property.rs @@ -3,13 +3,15 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::sync::Arc; +use comemo::Prehashed; + use super::{Interruption, NodeId, StyleChain}; use crate::eval::{RawLength, Smart}; use crate::geom::{Corners, Length, Numeric, Relative, Sides, Spec}; use crate::library::layout::PageNode; use crate::library::structure::{EnumNode, ListNode}; use crate::library::text::ParNode; -use crate::util::{Prehashed, ReadableTypeId}; +use crate::util::ReadableTypeId; /// A style property originating from a set rule or constructor. #[derive(Clone, Hash)] diff --git a/src/model/recipe.rs b/src/model/recipe.rs index 980d939b9..6b21ccf20 100644 --- a/src/model/recipe.rs +++ b/src/model/recipe.rs @@ -1,5 +1,7 @@ use std::fmt::{self, Debug, Formatter}; +use comemo::Tracked; + use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntry}; use crate::diag::SourceResult; use crate::eval::{Args, Func, Regex, Value}; @@ -29,7 +31,7 @@ impl Recipe { /// Try to apply the recipe to the target. pub fn apply( &self, - world: &dyn World, + world: Tracked, styles: StyleChain, sel: Selector, target: Target, @@ -75,7 +77,7 @@ impl Recipe { } /// Call the recipe function, with the argument if desired. - fn call(&self, world: &dyn World, arg: F) -> SourceResult + fn call(&self, world: Tracked, arg: F) -> SourceResult where F: FnOnce() -> Value, { diff --git a/src/model/show.rs b/src/model/show.rs index 56fb29ba4..b30b22643 100644 --- a/src/model/show.rs +++ b/src/model/show.rs @@ -2,10 +2,11 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; +use comemo::{Prehashed, Tracked}; + use super::{Content, NodeId, Selector, StyleChain}; use crate::diag::SourceResult; use crate::eval::Dict; -use crate::util::Prehashed; use crate::World; /// A node that can be realized given some styles. @@ -18,7 +19,11 @@ pub trait Show: 'static { /// The base recipe for this node that is executed if there is no /// user-defined show rule. - fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult; + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult; /// Finalize this node given the realization of a base or user recipe. Use /// this for effects that should work even in the face of a user-defined @@ -30,7 +35,7 @@ pub trait Show: 'static { #[allow(unused_variables)] fn finalize( &self, - world: &dyn World, + world: Tracked, styles: StyleChain, realized: Content, ) -> SourceResult { @@ -74,13 +79,17 @@ impl Show for ShowNode { self.0.encode(styles) } - fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult { + fn realize( + &self, + world: Tracked, + styles: StyleChain, + ) -> SourceResult { self.0.realize(world, styles) } fn finalize( &self, - world: &dyn World, + world: Tracked, styles: StyleChain, realized: Content, ) -> SourceResult { diff --git a/src/model/styles.rs b/src/model/styles.rs index b61bd5358..93b615fc1 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -3,6 +3,8 @@ use std::hash::Hash; use std::iter; use std::marker::PhantomData; +use comemo::Tracked; + use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target}; use crate::diag::SourceResult; use crate::frame::Role; @@ -279,7 +281,7 @@ impl<'a> StyleChain<'a> { /// Apply show recipes in this style chain to a target. pub fn apply( self, - world: &dyn World, + world: Tracked, target: Target, ) -> SourceResult> { // Find out how many recipes there any and whether any of their patterns diff --git a/src/source.rs b/src/source.rs index b78d90522..0ada1b041 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,8 +1,11 @@ //! Source file management. +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::Range; use std::path::{Path, PathBuf}; +use comemo::Prehashed; use unscanny::Scanner; use crate::diag::SourceResult; @@ -13,15 +16,14 @@ use crate::util::{PathExt, StrExt}; /// A source file. /// -/// _Note_: All line and column indices start at zero, just like byte indices. -/// Only for user-facing display, you should add 1 to them. +/// All line and column indices start at zero, just like byte indices. Only for +/// user-facing display, you should add 1 to them. pub struct Source { id: SourceId, path: PathBuf, - text: String, + text: Prehashed, lines: Vec, root: SyntaxNode, - rev: usize, } impl Source { @@ -38,9 +40,8 @@ impl Source { id, path: path.normalize(), root, - text, + text: Prehashed::new(text), lines, - rev: 0, } } @@ -87,14 +88,6 @@ impl Source { &self.text } - /// The revision number of the file. - /// - /// This is increased on [replacements](Self::replace) and - /// [edits](Self::edit). - pub fn rev(&self) -> usize { - self.rev - } - /// Slice out the part of the source code enclosed by the range. pub fn get(&self, range: Range) -> Option<&str> { self.text.get(range) @@ -102,12 +95,11 @@ impl Source { /// Fully replace the source text and increase the revision number. pub fn replace(&mut self, text: String) { - self.text = text; + self.text = Prehashed::new(text); self.lines = vec![Line { byte_idx: 0, utf16_idx: 0 }]; self.lines.extend(lines(0, 0, &self.text)); self.root = parse(&self.text); self.root.numberize(self.id(), Span::FULL).unwrap(); - self.rev = self.rev.wrapping_add(1); } /// Edit the source file by replacing the given range and increase the @@ -117,11 +109,11 @@ impl Source { /// /// The method panics if the `replace` range is out of bounds. pub fn edit(&mut self, replace: Range, with: &str) -> Range { - self.rev = self.rev.wrapping_add(1); - let start_byte = replace.start; let start_utf16 = self.byte_to_utf16(replace.start).unwrap(); - self.text.replace_range(replace.clone(), with); + let mut text = std::mem::take(&mut self.text).into_inner(); + text.replace_range(replace.clone(), with); + self.text = Prehashed::new(text); // Remove invalidated line starts. let line = self.byte_to_line(start_byte).unwrap(); @@ -246,6 +238,20 @@ impl Source { } } +impl Debug for Source { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Source({})", self.path.display()) + } +} + +impl Hash for Source { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.path.hash(state); + self.text.hash(state); + } +} + /// A unique identifier for a loaded source file. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct SourceId(u16); @@ -256,16 +262,13 @@ impl SourceId { Self(u16::MAX) } - /// Create a source id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub const fn from_raw(v: u16) -> Self { + /// Create a source id from a number. + pub const fn from_u16(v: u16) -> Self { Self(v) } - /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u16 { + /// Extract the underlying number. + pub const fn into_u16(self) -> u16 { self.0 } } diff --git a/src/syntax/span.rs b/src/syntax/span.rs index 4eae4124c..59c4cc5c9 100644 --- a/src/syntax/span.rs +++ b/src/syntax/span.rs @@ -81,7 +81,7 @@ impl Span { "span number outside valid range" ); - let bits = ((id.into_raw() as u64) << Self::BITS) | number; + let bits = ((id.into_u16() as u64) << Self::BITS) | number; Self(to_non_zero(bits)) } @@ -98,7 +98,7 @@ impl Span { /// The id of the source file the span points into. pub const fn source(self) -> SourceId { - SourceId::from_raw((self.0.get() >> Self::BITS) as u16) + SourceId::from_u16((self.0.get() >> Self::BITS) as u16) } /// The unique number of the span within the source file. @@ -157,7 +157,7 @@ mod tests { #[test] fn test_span_encoding() { - let id = SourceId::from_raw(5); + let id = SourceId::from_u16(5); let span = Span::new(id, 10).with_pos(SpanPos::End); assert_eq!(span.source(), id); assert_eq!(span.number(), 10); diff --git a/src/util/buffer.rs b/src/util/buffer.rs index daee86f94..766b20845 100644 --- a/src/util/buffer.rs +++ b/src/util/buffer.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::Deref; use std::sync::Arc; -use super::Prehashed; +use comemo::Prehashed; /// A shared buffer that is cheap to clone and hash. #[derive(Clone, Hash, Eq, PartialEq)] diff --git a/src/util/hash.rs b/src/util/hash.rs deleted file mode 100644 index 79455918d..000000000 --- a/src/util/hash.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::any::Any; -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::ops::Deref; - -/// A wrapper around a type that precomputes its hash. -#[derive(Copy, Clone)] -pub struct Prehashed { - /// The precomputed hash. - hash: u64, - /// The wrapped item. - item: T, -} - -impl Prehashed { - /// Compute an item's hash and wrap it. - pub fn new(item: T) -> Self { - Self { - hash: { - // Also hash the TypeId because the type might be converted - // through an unsized coercion. - let mut state = fxhash::FxHasher64::default(); - item.type_id().hash(&mut state); - item.hash(&mut state); - state.finish() - }, - item, - } - } - - /// Return the wrapped value. - pub fn into_iter(self) -> T { - self.item - } -} - -impl Deref for Prehashed { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.item - } -} - -impl Debug for Prehashed { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.item.fmt(f) - } -} - -impl Hash for Prehashed { - fn hash(&self, state: &mut H) { - state.write_u64(self.hash); - } -} - -impl Eq for Prehashed {} - -impl PartialEq for Prehashed { - fn eq(&self, other: &Self) -> bool { - self.hash == other.hash - } -} diff --git a/src/util/mod.rs b/src/util/mod.rs index 4a48b1d32..d549fc5f2 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -3,11 +3,9 @@ #[macro_use] mod eco; mod buffer; -mod hash; pub use buffer::Buffer; pub use eco::EcoString; -pub use hash::Prehashed; use std::any::TypeId; use std::fmt::{self, Debug, Formatter}; diff --git a/tests/typ/code/import.typ b/tests/typ/code/import.typ index 0c0b6c086..b554d6e7a 100644 --- a/tests/typ/code/import.typ +++ b/tests/typ/code/import.typ @@ -38,7 +38,7 @@ #import a, c, from "target.typ" --- -// Error: 19-21 file not found (searched at typ/code) +// Error: 19-21 failed to load file (is a directory) #import name from "" --- diff --git a/tests/typeset.rs b/tests/typeset.rs index d3d365c3f..b6cb217de 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -1,15 +1,15 @@ -use std::cell::RefCell; -use std::collections::{hash_map::Entry, HashMap}; +use std::cell::{RefCell, RefMut}; +use std::collections::HashMap; use std::env; use std::ffi::OsStr; use std::fs::{self, File}; -use std::hash::Hash; +use std::io::Read; use std::ops::Range; use std::path::{Path, PathBuf}; +use comemo::Prehashed; use elsa::FrozenVec; -use same_file::Handle; -use siphasher::sip128::{Hasher128, SipHasher}; +use once_cell::unsync::OnceCell; use tiny_skia as sk; use unscanny::Scanner; use walkdir::WalkDir; @@ -24,7 +24,7 @@ use typst::library::text::{TextNode, TextSize}; use typst::model::StyleMap; use typst::source::{Source, SourceId}; use typst::syntax::SyntaxNode; -use typst::util::Buffer; +use typst::util::{Buffer, PathExt}; use typst::{bail, Config, World}; const TYP_DIR: &str = "./typ"; @@ -147,54 +147,65 @@ impl Args { } } +fn config() -> Config { + // Set page width to 120pt with 10pt margins, so that the inner page is + // exactly 100pt wide. Page height is unbounded and font size is 10pt so + // that it multiplies to nice round numbers. + let mut styles = StyleMap::new(); + styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into())); + styles.set(PageNode::HEIGHT, Smart::Auto); + styles.set( + PageNode::MARGINS, + Sides::splat(Some(Smart::Custom(Length::pt(10.0).into()))), + ); + styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into())); + + // Hook up helpers into the global scope. + let mut std = typst::library::new(); + std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); + std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); + std.def_fn("test", move |_, args| { + let lhs = args.expect::("left-hand side")?; + let rhs = args.expect::("right-hand side")?; + if lhs != rhs { + bail!(args.span, "Assertion failed: {:?} != {:?}", lhs, rhs,); + } + Ok(Value::None) + }); + std.def_fn("print", move |_, args| { + print!("> "); + for (i, value) in args.all::()?.into_iter().enumerate() { + if i > 0 { + print!(", ") + } + print!("{value:?}"); + } + println!(); + Ok(Value::None) + }); + + Config { root: PathBuf::new(), std, styles } +} + +/// A world that provides access to the tests environment. struct TestWorld { - config: Config, print: PrintConfig, - sources: FrozenVec>, - nav: RefCell>, - book: FontBook, + config: Prehashed, + book: Prehashed, fonts: Vec, - files: RefCell>, + paths: RefCell>, + sources: FrozenVec>, +} + +#[derive(Default)] +struct PathSlot { + source: OnceCell>, + buffer: OnceCell>, } impl TestWorld { fn new(print: PrintConfig) -> Self { - // Set page width to 120pt with 10pt margins, so that the inner page is - // exactly 100pt wide. Page height is unbounded and font size is 10pt so - // that it multiplies to nice round numbers. - let mut styles = StyleMap::new(); - styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into())); - styles.set(PageNode::HEIGHT, Smart::Auto); - styles.set( - PageNode::MARGINS, - Sides::splat(Some(Smart::Custom(Length::pt(10.0).into()))), - ); - styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into())); - - // Hook up helpers into the global scope. - let mut std = typst::library::new(); - std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); - std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); - std.def_fn("test", move |_, args| { - let lhs = args.expect::("left-hand side")?; - let rhs = args.expect::("right-hand side")?; - if lhs != rhs { - bail!(args.span, "Assertion failed: {:?} != {:?}", lhs, rhs,); - } - Ok(Value::None) - }); - std.def_fn("print", move |_, args| { - print!("> "); - for (i, value) in args.all::()?.into_iter().enumerate() { - if i > 0 { - print!(", ") - } - print!("{value:?}"); - } - println!(); - Ok(Value::None) - }); - + // Search for fonts. let mut fonts = vec![]; for entry in WalkDir::new(FONT_DIR) .into_iter() @@ -208,56 +219,22 @@ impl TestWorld { } Self { - config: Config { root: PathBuf::new(), std, styles }, print, - sources: FrozenVec::new(), - nav: RefCell::new(HashMap::new()), - book: FontBook::from_fonts(&fonts), + config: Prehashed::new(config()), + book: Prehashed::new(FontBook::from_fonts(&fonts)), fonts, - files: RefCell::new(HashMap::new()), + paths: RefCell::default(), + sources: FrozenVec::new(), } } - - fn provide(&mut self, path: &Path, text: String) -> SourceId { - let hash = PathHash::new(path).unwrap(); - if let Some(&id) = self.nav.borrow().get(&hash) { - self.sources.as_mut()[id.into_raw() as usize].replace(text); - return id; - } - - let id = SourceId::from_raw(self.sources.len() as u16); - let source = Source::new(id, path, text); - self.sources.push(Box::new(source)); - self.nav.borrow_mut().insert(hash, id); - id - } } impl World for TestWorld { - fn config(&self) -> &Config { + fn config(&self) -> &Prehashed { &self.config } - fn resolve(&self, path: &Path) -> FileResult { - let hash = PathHash::new(path)?; - if let Some(&id) = self.nav.borrow().get(&hash) { - return Ok(id); - } - - let text = fs::read_to_string(path).map_err(|e| FileError::from_io(e, path))?; - let id = SourceId::from_raw(self.sources.len() as u16); - let source = Source::new(id, path, text); - self.sources.push(Box::new(source)); - self.nav.borrow_mut().insert(hash, id); - - Ok(id) - } - - fn source(&self, id: SourceId) -> &Source { - &self.sources[id.into_raw() as usize] - } - - fn book(&self) -> &FontBook { + fn book(&self) -> &Prehashed { &self.book } @@ -266,33 +243,67 @@ impl World for TestWorld { } fn file(&self, path: &Path) -> FileResult { - let hash = PathHash::new(path)?; - Ok(match self.files.borrow_mut().entry(hash) { - Entry::Occupied(entry) => entry.get().clone(), - Entry::Vacant(entry) => entry - .insert(fs::read(path).map_err(|e| FileError::from_io(e, path))?.into()) - .clone(), - }) + self.slot(path) + .buffer + .get_or_init(|| read(path).map(Buffer::from)) + .clone() + } + + fn resolve(&self, path: &Path) -> FileResult { + self.slot(path) + .source + .get_or_init(|| { + let buf = read(path)?; + let text = String::from_utf8(buf)?; + Ok(self.insert(path, text)) + }) + .clone() + } + + fn source(&self, id: SourceId) -> &Source { + &self.sources[id.into_u16() as usize] } } -/// A hash that is the same for all paths pointing to the same file. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -struct PathHash(u128); - -impl PathHash { - fn new(path: &Path) -> FileResult { - let f = |e| FileError::from_io(e, path); - let file = File::open(path).map_err(f)?; - if file.metadata().map_err(f)?.is_file() { - let handle = Handle::from_file(file).map_err(f)?; - let mut state = SipHasher::new(); - handle.hash(&mut state); - Ok(Self(state.finish128().as_u128())) +impl TestWorld { + fn set(&mut self, path: &Path, text: String) -> SourceId { + let slot = self.slot(path); + if let Some(&Ok(id)) = slot.source.get() { + drop(slot); + self.sources.as_mut()[id.into_u16() as usize].replace(text); + id } else { - Err(FileError::NotFound(path.into())) + let id = self.insert(path, text); + slot.source.set(Ok(id)).unwrap(); + id } } + + fn slot(&self, path: &Path) -> RefMut { + RefMut::map(self.paths.borrow_mut(), |paths| { + paths.entry(path.normalize()).or_default() + }) + } + + fn insert(&self, path: &Path, text: String) -> SourceId { + let id = SourceId::from_u16(self.sources.len() as u16); + let source = Source::new(id, path, text); + self.sources.push(Box::new(source)); + id + } +} + +/// Read a file. +fn read(path: &Path) -> FileResult> { + let f = |e| FileError::from_io(e, path); + let mut file = File::open(path).map_err(f)?; + if file.metadata().map_err(f)?.is_file() { + let mut data = vec![]; + file.read_to_end(&mut data).map_err(f)?; + Ok(data) + } else { + Err(FileError::IsDirectory) + } } fn test( @@ -395,7 +406,7 @@ fn test_part( ) -> (bool, bool, Vec) { let mut ok = true; - let id = world.provide(src_path, text); + let id = world.set(src_path, text); let source = world.source(id); if world.print.syntax { println!("Syntax Tree: {:#?}", source.root())