summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-09-21 17:50:58 +0200
committerLaurenz <laurmaedje@gmail.com>2022-09-21 20:25:57 +0200
commitddd3b6a82b8c0353c942bfba8b89ca5476eedc58 (patch)
treea64c350f0f1f82152ff18cfb02fbfdbf39292672
parent3760748fddd3b793c79c370398a9d4a3fc5afc04 (diff)
Tracked memoization
-rw-r--r--Cargo.lock284
-rw-r--r--Cargo.toml37
-rw-r--r--benches/oneshot.rs44
-rw-r--r--src/diag.rs18
-rw-r--r--src/eval/func.rs24
-rw-r--r--src/eval/mod.rs57
-rw-r--r--src/eval/vm.rs33
-rw-r--r--src/font/book.rs6
-rw-r--r--src/lib.rs34
-rw-r--r--src/library/graphics/hide.rs2
-rw-r--r--src/library/graphics/image.rs2
-rw-r--r--src/library/graphics/line.rs2
-rw-r--r--src/library/graphics/shape.rs2
-rw-r--r--src/library/graphics/transform.rs4
-rw-r--r--src/library/layout/align.rs2
-rw-r--r--src/library/layout/columns.rs2
-rw-r--r--src/library/layout/flow.rs4
-rw-r--r--src/library/layout/grid.rs6
-rw-r--r--src/library/layout/pad.rs2
-rw-r--r--src/library/layout/page.rs4
-rw-r--r--src/library/layout/place.rs2
-rw-r--r--src/library/layout/stack.rs4
-rw-r--r--src/library/math/mod.rs8
-rw-r--r--src/library/math/rex.rs2
-rw-r--r--src/library/prelude.rs1
-rw-r--r--src/library/structure/doc.rs2
-rw-r--r--src/library/structure/heading.rs10
-rw-r--r--src/library/structure/list.rs10
-rw-r--r--src/library/structure/reference.rs2
-rw-r--r--src/library/structure/table.rs15
-rw-r--r--src/library/text/deco.rs6
-rw-r--r--src/library/text/link.rs4
-rw-r--r--src/library/text/mod.rs4
-rw-r--r--src/library/text/par.rs18
-rw-r--r--src/library/text/raw.rs8
-rw-r--r--src/library/text/repeat.rs2
-rw-r--r--src/library/text/shaping.rs12
-rw-r--r--src/library/text/shift.rs8
-rw-r--r--src/library/utility/mod.rs7
-rw-r--r--src/main.rs478
-rw-r--r--src/model/content.rs18
-rw-r--r--src/model/layout.rs16
-rw-r--r--src/model/property.rs4
-rw-r--r--src/model/recipe.rs6
-rw-r--r--src/model/show.rs19
-rw-r--r--src/model/styles.rs4
-rw-r--r--src/source.rs55
-rw-r--r--src/syntax/span.rs6
-rw-r--r--src/util/buffer.rs2
-rw-r--r--src/util/hash.rs63
-rw-r--r--src/util/mod.rs2
-rw-r--r--tests/typ/code/import.typ2
-rw-r--r--tests/typeset.rs227
53 files changed, 1063 insertions, 533 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 407ff66a..ccaf9391 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -90,6 +99,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -108,6 +123,18 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -124,6 +151,29 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -133,6 +183,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -209,6 +279,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -231,6 +313,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -275,6 +366,20 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -291,6 +396,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -318,6 +443,35 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -400,6 +554,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -411,6 +577,24 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -1013,6 +1200,60 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1050,6 +1291,49 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index b56e8991..65afa214 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 50fee9b1..23f829b3 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<Config>,
+ book: Prehashed<FontBook>,
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<dyn World> {
+ (self as &dyn World).track()
+ }
}
impl World for BenchWorld {
- fn config(&self) -> &Config {
+ fn config(&self) -> &Prehashed<Config> {
&self.config
}
- fn resolve(&self, path: &Path) -> FileResult<SourceId> {
- Err(FileError::NotFound(path.into()))
- }
-
- fn source(&self, _: SourceId) -> &Source {
- &self.source
- }
-
- fn book(&self) -> &FontBook {
+ fn book(&self) -> &Prehashed<FontBook> {
&self.book
}
@@ -139,4 +139,12 @@ impl World for BenchWorld {
fn file(&self, path: &Path) -> FileResult<Buffer> {
Err(FileError::NotFound(path.into()))
}
+
+ fn resolve(&self, path: &Path) -> FileResult<SourceId> {
+ Err(FileError::NotFound(path.into()))
+ }
+
+ fn source(&self, _: SourceId) -> &Source {
+ &self.source
+ }
}
diff --git a/src/diag.rs b/src/diag.rs
index ebd192c2..81bb7e51 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<T> {
/// Add the tracepoint to all errors that lie outside the `span`.
- fn trace<F>(self, world: &dyn World, make_point: F, span: Span) -> Self
+ fn trace<F>(self, world: Tracked<dyn World>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint;
}
impl<T> Trace<T> for SourceResult<T> {
- fn trace<F>(self, world: &dyn World, make_point: F, span: Span) -> Self
+ fn trace<F>(self, world: Tracked<dyn World>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint,
{
@@ -146,6 +149,8 @@ pub type FileResult<T> = Result<T, FileError>;
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<FromUtf8Error> for FileError {
+ fn from(_: FromUtf8Error) -> Self {
+ Self::InvalidUtf8
+ }
+}
+
impl From<FileError> for String {
fn from(error: FileError) -> Self {
error.to_string()
diff --git a/src/eval/func.rs b/src/eval/func.rs
index b8730a8a..c307b237 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<Value> {
- let mut vm = Vm::new(world, vec![], Scopes::new(None));
+ pub fn call_detached(
+ &self,
+ world: Tracked<dyn World>,
+ args: Args,
+ ) -> SourceResult<Value> {
+ 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 eeb95534..fb65420d 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<dyn World>,
+ route: Tracked<Route>,
id: SourceId,
- mut route: Vec<SourceId>,
) -> SourceResult<Module> {
// 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<Tracked<'static, Self>>,
+ id: Option<SourceId>,
+}
+
+impl Route {
+ /// Create a new, empty route.
+ pub fn new(id: Option<SourceId>) -> 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<Route>, 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<Module> {
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 7c8e8e31..0604e7be 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<SourceId>,
+ pub route: Tracked<'a, Route>,
+ /// The current location.
+ pub location: Option<SourceId>,
/// The stack of scopes.
- pub scopes: Scopes<'w>,
+ pub scopes: Scopes<'a>,
/// A control flow event that is currently happening.
pub flow: Option<Flow>,
}
-impl<'w> Vm<'w> {
+impl<'a> Vm<'a> {
/// Create a new virtual machine.
- pub fn new(ctx: &'w dyn World, route: Vec<SourceId>, 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<SourceId>,
+ 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<PathBuf> {
- 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 323eb84f..29190516 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<String, Vec<usize>>,
@@ -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<u32>);
diff --git a/src/lib.rs b/src/lib.rs
index 6122ffed..e288d556 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<Vec<Frame>> {
- let module = eval::evaluate(world, main, vec![])?;
- model::layout(world, &module.content)
+pub fn typeset(
+ world: &(dyn World + 'static),
+ main: SourceId,
+) -> SourceResult<Vec<Frame>> {
+ 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<SourceId>;
-
- /// Access a source file by id.
- fn source(&self, id: SourceId) -> &Source;
+ fn config(&self) -> &Prehashed<Config>;
/// Metadata about all known fonts.
- fn book(&self) -> &FontBook;
+ fn book(&self) -> &Prehashed<FontBook>;
/// Try to access the font with the given id.
fn font(&self, id: usize) -> Option<Font>;
/// Try to access a file at a path.
fn file(&self, path: &Path) -> FileResult<Buffer>;
+
+ /// Try to resolve the unique id of a source file.
+ fn resolve(&self, path: &Path) -> FileResult<SourceId>;
+
+ /// 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 505dd1f6..65684272 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs
index c0249b3c..7523471d 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs
index ebfec1b2..192f8350 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index d9162557..eea02568 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -78,7 +78,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
impl<const S: ShapeKind> Layout for ShapeNode<S> {
fn layout(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs
index 34d45bd0..b110f343 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -86,7 +86,7 @@ impl<const T: TransformKind> TransformNode<T> {
impl<const T: TransformKind> Layout for TransformNode<T> {
fn layout(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs
index 0c758cf2..705d555b 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs
index e0163f63..8ae4394e 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs
index 05c10789..7cb52910 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -149,7 +149,7 @@ impl FlowLayouter {
/// Layout a node.
pub fn layout_node(
&mut self,
- world: &dyn World,
+ world: Tracked<dyn World>,
node: &LayoutNode,
styles: StyleChain,
) -> SourceResult<()> {
diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs
index cd4fc6b4..2c246df9 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -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 983bfa11..06c3672f 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
index ba597263..9cbbcca5 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<dyn World>,
mut page: usize,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -180,7 +180,7 @@ impl Marginal {
/// Resolve the marginal based on the page number.
pub fn resolve(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
page: usize,
) -> SourceResult<Option<Content>> {
Ok(match self {
diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs
index 862c969e..01da62e5 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs
index a9fc1621..b9a26642 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -168,7 +168,7 @@ impl<'a> StackLayouter<'a> {
/// Layout an arbitrary node.
pub fn layout_node(
&mut self,
- world: &dyn World,
+ world: Tracked<dyn World>,
node: &LayoutNode,
styles: StyleChain,
) -> SourceResult<()> {
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs
index d71f6976..1f5ea8f3 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<Content> {
+ fn realize(
+ &self,
+ _: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
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<dyn World>,
styles: StyleChain,
mut realized: Content,
) -> SourceResult<Content> {
diff --git a/src/library/math/rex.rs b/src/library/math/rex.rs
index 76ba5177..96e8e438 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<dyn World>,
_: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/prelude.rs b/src/library/prelude.rs
index 48eebaf6..44d1af7f 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 ba848b64..c3af3f1c 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<dyn World>,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
let mut frames = vec![];
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs
index 855c0503..01738496 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<Content> {
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
Ok(Content::block(self.body.clone()))
}
fn finalize(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
styles: StyleChain,
mut realized: Content,
) -> SourceResult<Content> {
@@ -149,7 +149,11 @@ pub enum Leveled<T> {
impl<T: Cast + Clone> Leveled<T> {
/// Resolve the value based on the level.
- pub fn resolve(&self, world: &dyn World, level: NonZeroUsize) -> SourceResult<T> {
+ pub fn resolve(
+ &self,
+ world: Tracked<dyn World>,
+ level: NonZeroUsize,
+ ) -> SourceResult<T> {
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 f63374f3..9da14733 100644
--- a/src/library/structure/list.rs
+++ b/src/library/structure/list.rs
@@ -100,7 +100,11 @@ impl<const L: ListKind> Show for ListNode<L> {
}
}
- fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult<Content> {
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
let mut cells = vec![];
let mut number = self.start;
@@ -145,7 +149,7 @@ impl<const L: ListKind> Show for ListNode<L> {
fn finalize(
&self,
- _: &dyn World,
+ _: Tracked<dyn World>,
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
@@ -208,7 +212,7 @@ impl Label {
/// Resolve the value based on the level.
pub fn resolve(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
kind: ListKind,
number: usize,
) -> SourceResult<Content> {
diff --git a/src/library/structure/reference.rs b/src/library/structure/reference.rs
index 5d1dab38..0a9f4f9c 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<Content> {
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
Ok(Content::Text(format_eco!("@{}", self.0)))
}
}
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index f1ca7e03..9f89cd2b 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<Content> {
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
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<dyn World>,
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
@@ -129,7 +133,12 @@ pub enum Celled<T> {
impl<T: Cast + Clone> Celled<T> {
/// Resolve the value based on the cell position.
- pub fn resolve(&self, world: &dyn World, x: usize, y: usize) -> SourceResult<T> {
+ pub fn resolve(
+ &self,
+ world: Tracked<dyn World>,
+ x: usize,
+ y: usize,
+ ) -> SourceResult<T> {
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 3d030d45..1242488b 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -48,7 +48,11 @@ impl<const L: DecoLine> Show for DecoNode<L> {
dict! { "body" => Value::Content(self.0.clone()) }
}
- fn realize(&self, _: &dyn World, styles: StyleChain) -> SourceResult<Content> {
+ fn realize(
+ &self,
+ _: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
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 f89bbd67..c06fea55 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<Content> {
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
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<dyn World>,
styles: StyleChain,
mut realized: Content,
) -> SourceResult<Content> {
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
index 55b866cb..934f5e15 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<Content> {
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
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<Content> {
+ fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(TextNode::ITALIC, Toggle))
}
}
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 00a1e034..6910c23a 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -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<dyn World>,
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<Item<'a>>,
- world: &dyn World,
+ world: Tracked<dyn World>,
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<dyn World>,
width: Length,
) -> Vec<Line<'a>> {
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<dyn World>,
width: Length,
) -> Vec<Line<'a>> {
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<dyn World>,
width: Length,
) -> Vec<Line<'a>> {
/// 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<dyn World>,
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<dyn World>,
lines: &[Line],
regions: &Regions,
) -> SourceResult<Vec<Frame>> {
@@ -1072,7 +1072,7 @@ fn stack(
/// Commit to a line and build its frame.
fn commit(
p: &Preparation,
- world: &dyn World,
+ world: Tracked<dyn World>,
line: &Line,
regions: &Regions,
width: Length,
diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs
index 5bce2a90..e7c73a91 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<Content> {
+ fn realize(
+ &self,
+ _: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
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<dyn World>,
styles: StyleChain,
mut realized: Content,
) -> SourceResult<Content> {
diff --git a/src/library/text/repeat.rs b/src/library/text/repeat.rs
index 78a21069..e3bae3fc 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs
index c1d0341b..16989acf 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<dyn World>, 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<dyn World>) -> (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<dyn World>,
text_range: Range<usize>,
) -> 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<dyn World>) {
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<ShapedGlyph>,
used: Vec<Font>,
styles: StyleChain<'a>,
@@ -319,7 +319,7 @@ struct ShapingContext<'a> {
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
- world: &dyn World,
+ world: Tracked<dyn World>,
text: &'a str,
styles: StyleChain<'a>,
dir: Dir,
diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs
index b359c5ed..6a5415e8 100644
--- a/src/library/text/shift.rs
+++ b/src/library/text/shift.rs
@@ -42,7 +42,11 @@ impl<const S: ScriptKind> Show for ShiftNode<S> {
dict! { "body" => Value::Content(self.0.clone()) }
}
- fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult<Content> {
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
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<EcoString> {
/// 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<dyn World>, 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 3fc413f7..d9b19d64 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<Value> {
// 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 9a6e367f..1c5790c2 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<T> = Result<T, CodespanError>;
@@ -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<PathBuf>,
+ 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 <dir> 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] <input.typ> [output.html]
-
-ARGS:
- <input.typ> 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<Command> {
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<Command> {
}
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<Box<Source>>,
- nav: RefCell<HashMap<PathHash, SourceId>>,
- book: FontBook,
+ config: Prehashed<Config>,
+ book: Prehashed<FontBook>,
fonts: Vec<FontSlot>,
- files: RefCell<HashMap<PathHash, Buffer>>,
+ hashes: RefCell<HashMap<PathBuf, FileResult<PathHash>>>,
+ paths: RefCell<HashMap<PathHash, PathSlot>>,
+ sources: FrozenVec<Box<Source>>,
}
+/// Holds details about the location of a font and lazily the font itself.
struct FontSlot {
path: PathBuf,
index: u32,
font: OnceCell<Option<Font>>,
}
+/// Holds canonical data for all paths pointing to the same entity.
+#[derive(Default)]
+struct PathSlot {
+ source: OnceCell<FileResult<SourceId>>,
+ buffer: OnceCell<FileResult<Buffer>>,
+}
+
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<Config> {
&self.config
}
- fn resolve(&self, path: &Path) -> FileResult<SourceId> {
- 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<FontBook> {
&self.book
}
@@ -360,36 +424,178 @@ impl World for SystemWorld {
}
fn file(&self, path: &Path) -> FileResult<Buffer> {
- 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<SourceId> {
+ 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]
+ }
+}
+
+impl SystemWorld {
+ fn slot(&self, path: &Path) -> FileResult<RefMut<PathSlot>> {
+ 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: &notify::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 file.
+/// 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<Self> {
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<Vec<u8>> {
+ 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<Self::Name> {
+ Ok(World::source(self, id).path().display())
+ }
+
+ fn source(&'a self, id: SourceId) -> CodespanResult<Self::Source> {
+ Ok(World::source(self, id).text())
+ }
+
+ fn line_index(&'a self, id: SourceId, given: usize) -> CodespanResult<usize> {
+ 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<std::ops::Range<usize>> {
+ 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<usize> {
+ 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<FontSlot>,
+}
+
+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<Self::Name> {
- Ok(World::source(self, id).path().display())
- }
-
- fn source(&'a self, id: SourceId) -> CodespanResult<Self::Source> {
- Ok(World::source(self, id).text())
- }
-
- fn line_index(&'a self, id: SourceId, given: usize) -> CodespanResult<usize> {
- 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<std::ops::Range<usize>> {
- 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<usize> {
- 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 dbea141c..92d592a6 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<Vec<Frame>> {
+#[comemo::memoize]
+pub fn layout(world: Tracked<dyn World>, content: &Content) -> SourceResult<Vec<Frame>> {
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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -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<Content>,
}
-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 68847471..8064afff 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<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>>;
@@ -214,9 +215,10 @@ impl LayoutNode {
}
impl Layout for LayoutNode {
+ #[comemo::memoize]
fn layout(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -285,7 +287,7 @@ struct EmptyNode;
impl Layout for EmptyNode {
fn layout(
&self,
- _: &dyn World,
+ _: Tracked<dyn World>,
regions: &Regions,
_: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -307,7 +309,7 @@ struct SizedNode {
impl Layout for SizedNode {
fn layout(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -354,7 +356,7 @@ struct FillNode {
impl Layout for FillNode {
fn layout(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
@@ -379,7 +381,7 @@ struct StrokeNode {
impl Layout for StrokeNode {
fn layout(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
regions: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
diff --git a/src/model/property.rs b/src/model/property.rs
index 8681da7d..18f41eee 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 980d939b..6b21ccf2 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<dyn World>,
styles: StyleChain,
sel: Selector,
target: Target,
@@ -75,7 +77,7 @@ impl Recipe {
}
/// Call the recipe function, with the argument if desired.
- fn call<F>(&self, world: &dyn World, arg: F) -> SourceResult<Content>
+ fn call<F>(&self, world: Tracked<dyn World>, arg: F) -> SourceResult<Content>
where
F: FnOnce() -> Value,
{
diff --git a/src/model/show.rs b/src/model/show.rs
index 56fb29ba..b30b2264 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<Content>;
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content>;
/// 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<dyn World>,
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
@@ -74,13 +79,17 @@ impl Show for ShowNode {
self.0.encode(styles)
}
- fn realize(&self, world: &dyn World, styles: StyleChain) -> SourceResult<Content> {
+ fn realize(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ ) -> SourceResult<Content> {
self.0.realize(world, styles)
}
fn finalize(
&self,
- world: &dyn World,
+ world: Tracked<dyn World>,
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
diff --git a/src/model/styles.rs b/src/model/styles.rs
index b61bd535..93b615fc 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<dyn World>,
target: Target,
) -> SourceResult<Option<Content>> {
// Find out how many recipes there any and whether any of their patterns
diff --git a/src/source.rs b/src/source.rs
index b78d9052..0ada1b04 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<String>,
lines: Vec<Line>,
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<usize>) -> 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<usize>, with: &str) -> Range<usize> {
- 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<H: Hasher>(&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 4eae4124..59c4cc5c 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 daee86f9..766b2084 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 79455918..00000000
--- 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<T: ?Sized> {
- /// The precomputed hash.
- hash: u64,
- /// The wrapped item.
- item: T,
-}
-
-impl<T: Hash + 'static> Prehashed<T> {
- /// 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<T: ?Sized> Deref for Prehashed<T> {
- type Target = T;
-
- fn deref(&self) -> &Self::Target {
- &self.item
- }
-}
-
-impl<T: Debug + ?Sized> Debug for Prehashed<T> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.item.fmt(f)
- }
-}
-
-impl<T: ?Sized> Hash for Prehashed<T> {
- fn hash<H: Hasher>(&self, state: &mut H) {
- state.write_u64(self.hash);
- }
-}
-
-impl<T: Eq + ?Sized> Eq for Prehashed<T> {}
-
-impl<T: ?Sized> PartialEq for Prehashed<T> {
- fn eq(&self, other: &Self) -> bool {
- self.hash == other.hash
- }
-}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 4a48b1d3..d549fc5f 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 0c0b6c08..b554d6e7 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 d3d365c3..b6cb217d 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::<Value>("left-hand side")?;
+ let rhs = args.expect::<Value>("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::<Value>()?.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<Box<Source>>,
- nav: RefCell<HashMap<PathHash, SourceId>>,
- book: FontBook,
+ config: Prehashed<Config>,
+ book: Prehashed<FontBook>,
fonts: Vec<Font>,
- files: RefCell<HashMap<PathHash, Buffer>>,
+ paths: RefCell<HashMap<PathBuf, PathSlot>>,
+ sources: FrozenVec<Box<Source>>,
+}
+
+#[derive(Default)]
+struct PathSlot {
+ source: OnceCell<FileResult<SourceId>>,
+ buffer: OnceCell<FileResult<Buffer>>,
}
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::<Value>("left-hand side")?;
- let rhs = args.expect::<Value>("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::<Value>()?.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,90 +219,90 @@ 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<Config> {
&self.config
}
- fn resolve(&self, path: &Path) -> FileResult<SourceId> {
- let hash = PathHash::new(path)?;
- if let Some(&id) = self.nav.borrow().get(&hash) {
- return Ok(id);
- }
+ fn book(&self) -> &Prehashed<FontBook> {
+ &self.book
+ }
- 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);
+ fn font(&self, id: usize) -> Option<Font> {
+ Some(self.fonts[id].clone())
+ }
- Ok(id)
+ fn file(&self, path: &Path) -> FileResult<Buffer> {
+ self.slot(path)
+ .buffer
+ .get_or_init(|| read(path).map(Buffer::from))
+ .clone()
}
- fn source(&self, id: SourceId) -> &Source {
- &self.sources[id.into_raw() as usize]
+ fn resolve(&self, path: &Path) -> FileResult<SourceId> {
+ self.slot(path)
+ .source
+ .get_or_init(|| {
+ let buf = read(path)?;
+ let text = String::from_utf8(buf)?;
+ Ok(self.insert(path, text))
+ })
+ .clone()
}
- fn book(&self) -> &FontBook {
- &self.book
+ fn source(&self, id: SourceId) -> &Source {
+ &self.sources[id.into_u16() as usize]
}
+}
- fn font(&self, id: usize) -> Option<Font> {
- Some(self.fonts[id].clone())
+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 {
+ let id = self.insert(path, text);
+ slot.source.set(Ok(id)).unwrap();
+ id
+ }
}
- fn file(&self, path: &Path) -> FileResult<Buffer> {
- 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(),
+ fn slot(&self, path: &Path) -> RefMut<PathSlot> {
+ 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
+ }
}
-/// 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<Self> {
- 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()))
- }
+/// Read a file.
+fn read(path: &Path) -> FileResult<Vec<u8>> {
+ 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)
}
}
@@ -395,7 +406,7 @@ fn test_part(
) -> (bool, bool, Vec<Frame>) {
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())