Fix and improve

- Set context location to resolved path during module evaluation.
- Dump module diagnostics on import
- Use same-file for more robustness than fs::canonicalize
This commit is contained in:
Laurenz 2021-05-31 12:00:13 +02:00
parent e023bf2ac9
commit 00ac68b845
5 changed files with 66 additions and 47 deletions

View File

@ -7,7 +7,7 @@ edition = "2018"
[features] [features]
default = ["cli", "fs"] default = ["cli", "fs"]
cli = ["anyhow", "fs"] cli = ["anyhow", "fs"]
fs = ["dirs", "memmap2", "walkdir"] fs = ["dirs", "memmap2", "same-file", "walkdir"]
[workspace] [workspace]
members = ["bench"] members = ["bench"]
@ -35,6 +35,7 @@ xi-unicode = "0.3"
anyhow = { version = "1", optional = true } anyhow = { version = "1", optional = true }
dirs = { version = "3", optional = true } dirs = { version = "3", optional = true }
memmap2 = { version = "0.2", optional = true } memmap2 = { version = "0.2", optional = true }
same-file = { version = "1", optional = true }
walkdir = { version = "2", optional = true } walkdir = { version = "2", optional = true }
[dev-dependencies] [dev-dependencies]

View File

@ -11,6 +11,7 @@ pub use scope::*;
pub use value::*; pub use value::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::mem;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
@ -65,10 +66,10 @@ pub struct EvalContext<'a> {
pub scopes: Scopes<'a>, pub scopes: Scopes<'a>,
/// Evaluation diagnostics. /// Evaluation diagnostics.
pub diags: DiagSet, pub diags: DiagSet,
/// The stack of imported files that led to evaluation of the current file.
pub route: Vec<FileHash>,
/// The location of the currently evaluated file. /// The location of the currently evaluated file.
pub path: PathBuf, pub path: PathBuf,
/// The stack of imported files that led to evaluation of the current file.
pub route: Vec<FileHash>,
/// A map of loaded module. /// A map of loaded module.
pub modules: HashMap<FileHash, Module>, pub modules: HashMap<FileHash, Module>,
} }
@ -91,8 +92,8 @@ impl<'a> EvalContext<'a> {
cache, cache,
scopes: Scopes::with_base(Some(base)), scopes: Scopes::with_base(Some(base)),
diags: DiagSet::new(), diags: DiagSet::new(),
route,
path: path.to_owned(), path: path.to_owned(),
route,
modules: HashMap::new(), modules: HashMap::new(),
} }
} }
@ -116,12 +117,13 @@ impl<'a> EvalContext<'a> {
pub fn import(&mut self, path: &str, span: Span) -> Option<FileHash> { pub fn import(&mut self, path: &str, span: Span) -> Option<FileHash> {
let (resolved, hash) = self.resolve(path, span)?; let (resolved, hash) = self.resolve(path, span)?;
// Prevent cycling importing. // Prevent cyclic importing.
if self.route.contains(&hash) { if self.route.contains(&hash) {
self.diag(error!(span, "cyclic import")); self.diag(error!(span, "cyclic import"));
return None; return None;
} }
// Check whether the module was already loaded.
if self.modules.get(&hash).is_some() { if self.modules.get(&hash).is_some() {
return Some(hash); return Some(hash);
} }
@ -136,19 +138,33 @@ impl<'a> EvalContext<'a> {
None None
})?; })?;
// Parse the file.
let parsed = parse(string);
// Prepare the new context. // Prepare the new context.
self.route.push(hash);
let new_scopes = Scopes::with_base(self.scopes.base); let new_scopes = Scopes::with_base(self.scopes.base);
let old_scopes = std::mem::replace(&mut self.scopes, new_scopes); 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);
self.route.push(hash);
// Evaluate the module. // Evaluate the module.
let tree = Rc::new(parse(string).output); let tree = Rc::new(parsed.output);
let map = tree.eval(self); let map = tree.eval(self);
// Restore the old context. // Restore the old context.
let new_scopes = std::mem::replace(&mut self.scopes, old_scopes); let new_scopes = mem::replace(&mut self.scopes, old_scopes);
let new_diags = mem::replace(&mut self.diags, old_diags);
self.path = old_path;
self.route.pop(); self.route.pop();
// Put all diagnostics from the module on the import.
for mut diag in new_diags {
diag.span = span;
self.diag(diag);
}
// Save the evaluated module.
self.modules.insert(hash, Module { self.modules.insert(hash, Module {
scope: new_scopes.top, scope: new_scopes.top,
template: vec![TemplateNode::Tree { tree, map }], template: vec![TemplateNode::Tree { tree, map }],
@ -430,7 +446,7 @@ impl BinaryExpr {
} }
}; };
let lhs = std::mem::take(&mut *mutable); let lhs = mem::take(&mut *mutable);
let types = (lhs.type_name(), rhs.type_name()); let types = (lhs.type_name(), rhs.type_name());
*mutable = op(lhs, rhs); *mutable = op(lhs, rhs);
@ -513,7 +529,7 @@ impl Eval for ClosureExpr {
Value::Func(FuncValue::new(name, move |ctx, args| { Value::Func(FuncValue::new(name, move |ctx, args| {
// Don't leak the scopes from the call site. Instead, we use the // Don't leak the scopes from the call site. Instead, we use the
// scope of captured variables we collected earlier. // scope of captured variables we collected earlier.
let prev = std::mem::take(&mut ctx.scopes); let prev = mem::take(&mut ctx.scopes);
ctx.scopes.top = captured.clone(); ctx.scopes.top = captured.clone();
for param in params.iter() { for param in params.iter() {
@ -657,11 +673,9 @@ impl Eval for ImportExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let span = self.path.span();
let path = self.path.eval(ctx); let path = self.path.eval(ctx);
if let Some(path) = ctx.cast::<String>(path, self.path.span()) {
if let Some(path) = ctx.cast::<String>(path, span) { if let Some(hash) = ctx.import(&path, self.path.span()) {
if let Some(hash) = ctx.import(&path, span) {
let mut module = &ctx.modules[&hash]; let mut module = &ctx.modules[&hash];
match &self.imports { match &self.imports {
Imports::Wildcard => { Imports::Wildcard => {
@ -695,11 +709,9 @@ impl Eval for IncludeExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let span = self.path.span();
let path = self.path.eval(ctx); let path = self.path.eval(ctx);
if let Some(path) = ctx.cast::<String>(path, self.path.span()) {
if let Some(path) = ctx.cast::<String>(path, span) { if let Some(hash) = ctx.import(&path, self.path.span()) {
if let Some(hash) = ctx.import(&path, span) {
return Value::Template(ctx.modules[&hash].template.clone()); return Value::Template(ctx.modules[&hash].template.clone());
} }
} }

View File

@ -16,20 +16,19 @@ use super::*;
/// - `ltr` /// - `ltr`
/// - `rtl` /// - `rtl`
pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
let iso = args.eat::<String>(ctx).map(|s| s.to_ascii_lowercase()); let iso = args.eat::<String>(ctx).map(|s| lang_dir(&s));
let dir = args.eat_named::<Spanned<Dir>>(ctx, "dir"); let dir = match args.eat_named::<Spanned<Dir>>(ctx, "dir") {
Some(dir) if dir.v.axis() == SpecAxis::Horizontal => Some(dir.v),
Some(dir) => {
ctx.diag(error!(dir.span, "must be horizontal"));
None
}
None => None,
};
Value::template("lang", move |ctx| { Value::template("lang", move |ctx| {
if let Some(iso) = &iso { if let Some(dir) = dir.or(iso) {
ctx.state.lang.dir = lang_dir(iso); ctx.state.lang.dir = dir;
}
if let Some(dir) = dir {
if dir.v.axis() == SpecAxis::Horizontal {
ctx.state.lang.dir = dir.v;
} else {
ctx.diag(error!(dir.span, "must be horizontal"));
}
} }
ctx.parbreak(); ctx.parbreak();
@ -38,7 +37,7 @@ pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
/// The default direction for the language identified by `iso`. /// The default direction for the language identified by `iso`.
fn lang_dir(iso: &str) -> Dir { fn lang_dir(iso: &str) -> Dir {
match iso { match iso.to_ascii_lowercase().as_str() {
"ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
"en" | "fr" | "de" | _ => Dir::LTR, "en" | "fr" | "de" | _ => Dir::LTR,
} }

View File

@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use memmap2::Mmap; use memmap2::Mmap;
use same_file::Handle;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ttf_parser::{name_id, Face}; use ttf_parser::{name_id, Face};
use walkdir::WalkDir; use walkdir::WalkDir;
@ -167,10 +168,6 @@ impl Loader for FsLoader {
&self.faces &self.faces
} }
fn resolve(&self, path: &Path) -> Option<FileHash> {
hash(path)
}
fn load_face(&mut self, idx: usize) -> Option<Buffer> { fn load_face(&mut self, idx: usize) -> Option<Buffer> {
load(&mut self.cache, &self.files[idx]) load(&mut self.cache, &self.files[idx])
} }
@ -178,6 +175,10 @@ impl Loader for FsLoader {
fn load_file(&mut self, path: &Path) -> Option<Buffer> { fn load_file(&mut self, path: &Path) -> Option<Buffer> {
load(&mut self.cache, path) load(&mut self.cache, path)
} }
fn resolve(&self, path: &Path) -> Option<FileHash> {
hash(path)
}
} }
/// Load from the file system using a cache. /// Load from the file system using a cache.
@ -191,8 +192,11 @@ fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> {
}) })
} }
/// Create a hash that is the same for all paths pointing to the same file.
fn hash(path: &Path) -> Option<FileHash> { fn hash(path: &Path) -> Option<FileHash> {
path.canonicalize().ok().map(|p| FileHash(fxhash::hash64(&p))) Handle::from_path(path)
.map(|handle| FileHash(fxhash::hash64(&handle)))
.ok()
} }
#[cfg(test)] #[cfg(test)]

View File

@ -19,19 +19,22 @@ pub trait Loader {
/// Descriptions of all font faces this loader serves. /// Descriptions of all font faces this loader serves.
fn faces(&self) -> &[FaceInfo]; fn faces(&self) -> &[FaceInfo];
/// Resolve a hash that is the same for all paths pointing to the same file.
///
/// Should return `None` if the file does not exist.
fn resolve(&self, path: &Path) -> Option<FileHash>;
/// Load the font face with the given index in [`faces()`](Self::faces). /// Load the font face with the given index in [`faces()`](Self::faces).
fn load_face(&mut self, idx: usize) -> Option<Buffer>; fn load_face(&mut self, idx: usize) -> Option<Buffer>;
/// Load a file from a path. /// Load a file from a path.
fn load_file(&mut self, path: &Path) -> Option<Buffer>; fn load_file(&mut self, path: &Path) -> Option<Buffer>;
/// Resolve a hash for the file the path points to.
///
/// This should return the same hash for all paths pointing to the same file
/// and `None` if the file does not exist.
fn resolve(&self, path: &Path) -> Option<FileHash>;
} }
/// A hash that must be the same for all paths pointing to the same file. /// A file hash that can be [resolved](Loader::resolve) from a path.
///
/// Should be the same for all paths pointing to the same file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct FileHash(pub u64); pub struct FileHash(pub u64);
@ -43,10 +46,6 @@ impl Loader for BlankLoader {
&[] &[]
} }
fn resolve(&self, _: &Path) -> Option<FileHash> {
None
}
fn load_face(&mut self, _: usize) -> Option<Buffer> { fn load_face(&mut self, _: usize) -> Option<Buffer> {
None None
} }
@ -54,4 +53,8 @@ impl Loader for BlankLoader {
fn load_file(&mut self, _: &Path) -> Option<Buffer> { fn load_file(&mut self, _: &Path) -> Option<Buffer> {
None None
} }
fn resolve(&self, _: &Path) -> Option<FileHash> {
None
}
} }