Refactor path handling

This commit is contained in:
Laurenz 2021-06-01 12:46:01 +02:00
parent 9bdb0bdeff
commit 7218892c72
17 changed files with 147 additions and 121 deletions

View File

@ -6,7 +6,7 @@ edition = "2018"
[features]
default = ["cli", "fs"]
cli = ["anyhow", "fs"]
cli = ["anyhow", "fs", "same-file"]
fs = ["dirs", "memmap2", "same-file", "walkdir"]
[workspace]

View File

@ -41,16 +41,16 @@ fn benchmarks(c: &mut Criterion) {
// Prepare intermediate results, run warm and fill caches.
let src = std::fs::read_to_string(&path).unwrap();
let tree = Rc::new(parse(&src).output);
let evaluated = eval(&mut loader, &mut cache, &path, tree.clone(), &scope);
let evaluated = eval(&mut loader, &mut cache, Some(&path), tree.clone(), &scope);
let executed = exec(&evaluated.output.template, state.clone());
let layouted = layout(&mut loader, &mut cache, &executed.output);
// Bench!
bench!("parse": parse(&src));
bench!("eval": eval(&mut loader, &mut cache, &path, tree.clone(), &scope));
bench!("eval": eval(&mut loader, &mut cache, Some(&path), tree.clone(), &scope));
bench!("exec": exec(&evaluated.output.template, state.clone()));
bench!("layout": layout(&mut loader, &mut cache, &executed.output));
bench!("typeset": typeset(&mut loader, &mut cache, &path, &src, &scope, state.clone()));
bench!("typeset": typeset(&mut loader, &mut cache, Some(&path), &src, &scope, state.clone()));
bench!("pdf": pdf(&cache, &layouted));
}
}

View File

@ -17,7 +17,7 @@ impl<'a> CapturesVisitor<'a> {
pub fn new(external: &'a Scopes) -> Self {
Self {
external,
internal: Scopes::new(),
internal: Scopes::new(None),
captures: Scope::new(),
}
}

View File

@ -23,22 +23,17 @@ use crate::loading::{FileHash, Loader};
use crate::parse::parse;
use crate::syntax::visit::Visit;
use crate::syntax::*;
use crate::util::PathExt;
/// Evaluated a parsed source file into a module.
///
/// The `path` should point to the source file for the `tree` and is used to
/// resolve relative path names.
///
/// The `scope` consists of the base definitions that are present from the
/// beginning (typically, the standard library).
/// Evaluate a parsed source file into a module.
pub fn eval(
loader: &mut dyn Loader,
cache: &mut Cache,
path: &Path,
path: Option<&Path>,
tree: Rc<Tree>,
base: &Scope,
scope: &Scope,
) -> Pass<Module> {
let mut ctx = EvalContext::new(loader, cache, path, base);
let mut ctx = EvalContext::new(loader, cache, path, scope);
let map = tree.eval(&mut ctx);
let module = Module {
scope: ctx.scopes.top,
@ -67,7 +62,7 @@ pub struct EvalContext<'a> {
/// Evaluation diagnostics.
pub diags: DiagSet,
/// The location of the currently evaluated file.
pub path: PathBuf,
pub path: Option<PathBuf>,
/// The stack of imported files that led to evaluation of the current file.
pub route: Vec<FileHash>,
/// A map of loaded module.
@ -79,20 +74,24 @@ impl<'a> EvalContext<'a> {
pub fn new(
loader: &'a mut dyn Loader,
cache: &'a mut Cache,
path: &Path,
base: &'a Scope,
path: Option<&Path>,
scope: &'a Scope,
) -> Self {
let path = path.map(PathExt::normalize);
let mut route = vec![];
if let Some(hash) = loader.resolve(path) {
route.push(hash);
if let Some(path) = &path {
if let Some(hash) = loader.resolve(path) {
route.push(hash);
}
}
Self {
loader,
cache,
scopes: Scopes::with_base(Some(base)),
scopes: Scopes::new(Some(scope)),
diags: DiagSet::new(),
path: path.to_owned(),
path,
route,
modules: HashMap::new(),
}
@ -102,10 +101,13 @@ impl<'a> EvalContext<'a> {
///
/// Generates an error if the file is not found.
pub fn resolve(&mut self, path: &str, span: Span) -> Option<(PathBuf, FileHash)> {
let dir = self.path.parent().expect("location is a file");
let path = dir.join(path);
let path = match &self.path {
Some(current) => current.parent()?.join(path),
None => PathBuf::from(path),
};
match self.loader.resolve(&path) {
Some(hash) => Some((path, hash)),
Some(hash) => Some((path.normalize(), hash)),
None => {
self.diag(error!(span, "file not found"));
None
@ -142,10 +144,10 @@ impl<'a> EvalContext<'a> {
let parsed = parse(string);
// Prepare the new context.
let new_scopes = Scopes::with_base(self.scopes.base);
let new_scopes = Scopes::new(self.scopes.base);
let old_scopes = mem::replace(&mut self.scopes, new_scopes);
let old_diags = mem::replace(&mut self.diags, parsed.diags);
let old_path = mem::replace(&mut self.path, resolved);
let old_path = mem::replace(&mut self.path, Some(resolved));
self.route.push(hash);
// Evaluate the module.

View File

@ -22,16 +22,7 @@ pub struct Scopes<'a> {
impl<'a> Scopes<'a> {
/// Create a new, empty hierarchy of scopes.
pub fn new() -> Self {
Self {
top: Scope::new(),
scopes: vec![],
base: None,
}
}
/// Create a new hierarchy of scopes with a base scope.
pub fn with_base(base: Option<&'a Scope>) -> Self {
pub fn new(base: Option<&'a Scope>) -> Self {
Self { top: Scope::new(), scopes: vec![], base }
}

View File

@ -664,7 +664,6 @@ impl From<AnyValue> for Value {
/// This would allow the type `FontFamily` to be cast from:
/// - a [`Value::Any`] variant already containing a `FontFamily`,
/// - a string, producing a named font family.
#[macro_export]
macro_rules! value {
($type:ty:
$type_name:literal

View File

@ -15,9 +15,6 @@ use crate::pretty::pretty;
use crate::syntax::*;
/// Execute a template to produce a layout tree.
///
/// The `state` is the base state that may be updated over the course of
/// execution.
pub fn exec(template: &TemplateValue, state: State) -> Pass<layout::Tree> {
let mut ctx = ExecContext::new(state);
template.exec(&mut ctx);
@ -53,7 +50,7 @@ impl ExecWithMap for Tree {
impl ExecWithMap for Node {
fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) {
match self {
Node::Text(text) => ctx.push_text(text.clone()),
Node::Text(text) => ctx.push_text(text),
Node::Space => ctx.push_word_space(),
_ => map[&(self as *const _)].exec(ctx),
}
@ -66,7 +63,7 @@ impl Exec for Value {
Value::None => {}
Value::Int(v) => ctx.push_text(pretty(v)),
Value::Float(v) => ctx.push_text(pretty(v)),
Value::Str(v) => ctx.push_text(v.clone()),
Value::Str(v) => ctx.push_text(v),
Value::Template(v) => v.exec(ctx),
Value::Error => {}
other => {
@ -93,7 +90,7 @@ impl Exec for TemplateNode {
fn exec(&self, ctx: &mut ExecContext) {
match self {
Self::Tree { tree, map } => tree.exec_with_map(ctx, &map),
Self::Str(v) => ctx.push_text(v.clone()),
Self::Str(v) => ctx.push_text(v),
Self::Func(v) => v.exec(ctx),
}
}

View File

@ -139,7 +139,7 @@ impl<'a> PdfExporter<'a> {
// We only write font switching actions when the used face changes. To
// do that, we need to remember the active face.
let mut face = FaceId::MAX;
let mut face = None;
let mut size = Length::zero();
let mut fill: Option<Fill> = None;
@ -158,8 +158,8 @@ impl<'a> PdfExporter<'a> {
// Then, also check if we need to issue a font switching
// action.
if shaped.face_id != face || shaped.size != size {
face = shaped.face_id;
if face != Some(shaped.face_id) || shaped.size != size {
face = Some(shaped.face_id);
size = shaped.size;
let name = format!("F{}", self.fonts.map(shaped.face_id));

View File

@ -6,8 +6,7 @@ use std::fmt::{self, Debug, Display, Formatter};
use serde::{Deserialize, Serialize};
use crate::geom::Length;
use crate::loading::Buffer;
use crate::loading::Loader;
use crate::loading::{Buffer, Loader};
/// A font face.
pub struct Face {
@ -171,7 +170,7 @@ impl FontCache {
let mut families = HashMap::<String, Vec<FaceId>>::new();
for (i, info) in loader.faces().iter().enumerate() {
let id = FaceId(i as u32);
let id = FaceId(i as u64);
faces.push(None);
families
.entry(info.family.to_lowercase())
@ -259,22 +258,19 @@ impl FontCache {
/// A unique identifier for a loaded font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct FaceId(u32);
pub struct FaceId(u64);
impl FaceId {
/// A blank initialization value.
pub const MAX: Self = Self(u32::MAX);
/// Create a face id from the raw underlying value.
///
/// This should only be called with values returned by
/// [`into_raw`](Self::into_raw).
pub fn from_raw(v: u32) -> Self {
pub fn from_raw(v: u64) -> Self {
Self(v)
}
/// Convert into the raw underlying value.
pub fn into_raw(self) -> u32 {
pub fn into_raw(self) -> u64 {
self.0
}
}

View File

@ -9,7 +9,7 @@ use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use serde::{Deserialize, Serialize};
use crate::loading::{FileHash, Loader};
use crate::loading::Loader;
/// A loaded image.
pub struct Image {
@ -55,10 +55,8 @@ impl Debug for Image {
/// Caches decoded images.
pub struct ImageCache {
/// Loaded images indexed by [`ImageId`].
images: Vec<Image>,
/// Maps from file hashes to ids of decoded images.
map: HashMap<FileHash, ImageId>,
images: HashMap<ImageId, Image>,
/// Callback for loaded images.
on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
@ -66,28 +64,22 @@ pub struct ImageCache {
impl ImageCache {
/// Create a new, empty image cache.
pub fn new() -> Self {
Self {
images: vec![],
map: HashMap::new(),
on_load: None,
}
Self { images: HashMap::new(), on_load: None }
}
/// Load and decode an image file from a path.
pub fn load(&mut self, loader: &mut dyn Loader, path: &Path) -> Option<ImageId> {
Some(match self.map.entry(loader.resolve(path)?) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let buffer = loader.load_file(path)?;
let image = Image::parse(&buffer)?;
let id = ImageId(self.images.len() as u32);
if let Some(callback) = &self.on_load {
callback(id, &image);
}
self.images.push(image);
*entry.insert(id)
let hash = loader.resolve(path)?;
let id = ImageId(hash.into_raw());
if let Entry::Vacant(entry) = self.images.entry(id) {
let buffer = loader.load_file(path)?;
let image = Image::parse(&buffer)?;
if let Some(callback) = &self.on_load {
callback(id, &image);
}
})
entry.insert(image);
}
Some(id)
}
/// Get a reference to a loaded image.
@ -96,7 +88,7 @@ impl ImageCache {
/// only be called with ids returned by [`load()`](Self::load).
#[track_caller]
pub fn get(&self, id: ImageId) -> &Image {
&self.images[id.0 as usize]
&self.images[&id]
}
/// Register a callback which is invoked each time an image is loaded.
@ -110,19 +102,19 @@ impl ImageCache {
/// A unique identifier for a loaded image.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ImageId(u32);
pub struct ImageId(u64);
impl ImageId {
/// Create an image id from the raw underlying value.
///
/// This should only be called with values returned by
/// [`into_raw`](Self::into_raw).
pub fn from_raw(v: u32) -> Self {
pub fn from_raw(v: u64) -> Self {
Self(v)
}
/// Convert into the raw underlying value.
pub fn into_raw(self) -> u32 {
pub fn into_raw(self) -> u64 {
self.0
}
}

View File

@ -59,16 +59,33 @@ use crate::layout::Frame;
use crate::loading::Loader;
/// Process source code directly into a collection of layouted frames.
///
/// # Parameters
/// - The `loader` is used to load fonts, images and other source files.
/// - The `cache` stores things that are reusable across several compilations
/// like loaded fonts, decoded images and layouting artifacts.
/// - The `path` should point to the source file if `src` comes from the file
/// system and is used to resolve relative paths (for importing and image
/// loading).
/// - The `src` is the _Typst_ source code to typeset.
/// - The `scope` contains definitions that are available everywhere,
/// typically the standard library.
/// - The `state` defines initial properties for page size, font selection and
/// so on.
///
/// # Return value
/// Returns a vector of frames representing individual pages alongside
/// diagnostic information (errors and warnings).
pub fn typeset(
loader: &mut dyn Loader,
cache: &mut Cache,
path: &Path,
path: Option<&Path>,
src: &str,
base: &Scope,
scope: &Scope,
state: State,
) -> Pass<Vec<Frame>> {
let parsed = parse::parse(src);
let evaluated = eval::eval(loader, cache, path, Rc::new(parsed.output), base);
let evaluated = eval::eval(loader, cache, path, Rc::new(parsed.output), scope);
let executed = exec::exec(&evaluated.output.template, state);
let layouted = layout::layout(loader, cache, &executed.output);

View File

@ -160,7 +160,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
let snapshot = ctx.state.clone();
ctx.set_monospace();
ctx.push_text(text.clone());
ctx.push_text(&text);
ctx.state = snapshot;
if block {

View File

@ -24,8 +24,7 @@ pub struct FsLoader {
cache: FileCache,
}
/// Maps from paths to loaded file buffers. When the buffer is `None` the file
/// does not exist or couldn't be read.
/// Maps from resolved file hashes to loaded file buffers.
type FileCache = HashMap<FileHash, Buffer>;
impl FsLoader {
@ -169,38 +168,29 @@ impl Loader for FsLoader {
}
fn load_face(&mut self, idx: usize) -> Option<Buffer> {
load(&mut self.cache, &self.files[idx])
self.load_file(&self.files[idx].clone())
}
fn load_file(&mut self, path: &Path) -> Option<Buffer> {
load(&mut self.cache, path)
let hash = self.resolve(path)?;
Some(Rc::clone(match self.cache.entry(hash) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let buffer = std::fs::read(path).ok()?;
entry.insert(Rc::new(buffer))
}
}))
}
fn resolve(&self, path: &Path) -> Option<FileHash> {
hash(path)
}
}
/// Load from the file system using a cache.
fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> {
Some(match cache.entry(hash(path)?) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => {
let buffer = std::fs::read(path).ok()?;
entry.insert(Rc::new(buffer)).clone()
let file = File::open(path).ok()?;
let meta = file.metadata().ok()?;
if meta.is_file() {
let handle = Handle::from_file(file).ok()?;
Some(FileHash::from_raw(fxhash::hash64(&handle)))
} else {
None
}
})
}
/// Create a hash that is the same for all paths pointing to the same file.
fn hash(path: &Path) -> Option<FileHash> {
let file = File::open(path).ok()?;
let meta = file.metadata().ok()?;
if meta.is_file() {
let handle = Handle::from_file(file).ok()?;
Some(FileHash(fxhash::hash64(&handle)))
} else {
None
}
}

View File

@ -36,7 +36,19 @@ pub trait Loader {
///
/// Should be the same for all paths pointing to the same file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct FileHash(pub u64);
pub struct FileHash(u64);
impl FileHash {
/// Create an file hash from a raw hash value.
pub fn from_raw(v: u64) -> Self {
Self(v)
}
/// Convert into the raw underlying hash value.
pub fn into_raw(self) -> u64 {
self.0
}
}
/// A loader which serves nothing.
pub struct BlankLoader;

View File

@ -2,8 +2,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context};
use typst::loading::Loader;
use same_file::is_same_file;
fn main() -> anyhow::Result<()> {
let args: Vec<_> = std::env::args().collect();
@ -33,9 +32,7 @@ fn main() -> anyhow::Result<()> {
};
// Ensure that the source file is not overwritten.
let src_hash = loader.resolve(&src_path);
let dest_hash = loader.resolve(&dest_path);
if src_hash.is_some() && src_hash == dest_hash {
if is_same_file(src_path, &dest_path).unwrap_or(false) {
bail!("source and destination files are the same");
}
@ -47,7 +44,14 @@ fn main() -> anyhow::Result<()> {
let mut cache = typst::cache::Cache::new(&loader);
let scope = typst::library::new();
let state = typst::exec::State::default();
let pass = typst::typeset(&mut loader, &mut cache, &src_path, &src, &scope, state);
let pass = typst::typeset(
&mut loader,
&mut cache,
Some(&src_path),
&src,
&scope,
state,
);
// Print diagnostics.
let map = typst::parse::LineMap::new(&src);

View File

@ -2,6 +2,7 @@
use std::cmp::Ordering;
use std::ops::Range;
use std::path::{Component, Path, PathBuf};
/// Additional methods for slices.
pub trait SliceExt<T> {
@ -79,3 +80,28 @@ impl RangeExt for Range<usize> {
}
}
}
/// Additional methods for [`Path`].
pub trait PathExt {
/// Lexically normalize a path.
fn normalize(&self) -> PathBuf;
}
impl PathExt for Path {
fn normalize(&self) -> PathBuf {
let mut out = PathBuf::new();
for component in self.components() {
match component {
Component::CurDir => {}
Component::ParentDir => match out.components().next_back() {
Some(Component::Normal(_)) => {
out.pop();
}
_ => out.push(component),
},
_ => out.push(component),
}
}
out
}
}

View File

@ -203,7 +203,7 @@ fn test(
fn test_part(
loader: &mut FsLoader,
cache: &mut Cache,
path: &Path,
src_path: &Path,
src: &str,
i: usize,
compare_ref: bool,
@ -224,7 +224,7 @@ fn test_part(
state.page.size = Size::new(Length::pt(120.0), Length::raw(f64::INFINITY));
state.page.margins = Sides::splat(Some(Length::pt(10.0).into()));
let mut pass = typst::typeset(loader, cache, path, &src, &scope, state);
let mut pass = typst::typeset(loader, cache, Some(src_path), &src, &scope, state);
if !compare_ref {
pass.output.clear();
}