Fatal errors

- Makes errors fatal, so that a phase is only reached when all previous phases were error-free
- Parsing still recovers and can produce multiple errors
- Evaluation fails fast and can thus produce only a single error (except for parse errors due to an import)
- The single error that could occur during execution is removed for now
- Removes Value::Error variant
This commit is contained in:
Laurenz 2021-07-30 18:04:08 +02:00
parent 42a27b48df
commit 1ee1d078e2
84 changed files with 1316 additions and 1473 deletions

View File

@ -4,7 +4,6 @@ use std::rc::Rc;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use typst::diag::Pass;
use typst::eval::{eval, Module}; use typst::eval::{eval, Module};
use typst::exec::exec; use typst::exec::exec;
use typst::export::pdf; use typst::export::pdf;
@ -25,9 +24,9 @@ fn benchmarks(c: &mut Criterion) {
for case in CASES { for case in CASES {
let path = Path::new(TYP_DIR).join(case); let path = Path::new(TYP_DIR).join(case);
let name = path.file_stem().unwrap().to_string_lossy(); let name = path.file_stem().unwrap().to_string_lossy();
let src_id = loader.resolve(&path).unwrap(); let file = loader.resolve(&path).unwrap();
let src = std::fs::read_to_string(&path).unwrap(); let src = std::fs::read_to_string(&path).unwrap();
let case = Case::new(src_id, src, ctx.clone()); let case = Case::new(file, src, ctx.clone());
macro_rules! bench { macro_rules! bench {
($step:literal, setup = |$ctx:ident| $setup:expr, code = $code:expr $(,)?) => { ($step:literal, setup = |$ctx:ident| $setup:expr, code = $code:expr $(,)?) => {
@ -80,7 +79,7 @@ fn benchmarks(c: &mut Criterion) {
/// A test case with prepared intermediate results. /// A test case with prepared intermediate results.
struct Case { struct Case {
ctx: Rc<RefCell<Context>>, ctx: Rc<RefCell<Context>>,
src_id: FileId, file: FileId,
src: String, src: String,
ast: Rc<SyntaxTree>, ast: Rc<SyntaxTree>,
module: Module, module: Module,
@ -89,16 +88,16 @@ struct Case {
} }
impl Case { impl Case {
fn new(src_id: FileId, src: String, ctx: Rc<RefCell<Context>>) -> Self { fn new(file: FileId, src: String, ctx: Rc<RefCell<Context>>) -> Self {
let mut borrowed = ctx.borrow_mut(); let mut borrowed = ctx.borrow_mut();
let ast = Rc::new(parse(&src).output); let ast = Rc::new(parse(file, &src).unwrap());
let module = eval(&mut borrowed, src_id, Rc::clone(&ast)).output; let module = eval(&mut borrowed, file, Rc::clone(&ast)).unwrap();
let tree = exec(&mut borrowed, &module.template).output; let tree = exec(&mut borrowed, &module.template);
let frames = layout(&mut borrowed, &tree); let frames = layout(&mut borrowed, &tree);
drop(borrowed); drop(borrowed);
Self { Self {
ctx, ctx,
src_id, file,
src, src,
ast, ast,
module, module,
@ -108,18 +107,14 @@ impl Case {
} }
fn parse(&self) -> SyntaxTree { fn parse(&self) -> SyntaxTree {
parse(&self.src).output parse(self.file, &self.src).unwrap()
} }
fn eval(&self) -> Pass<Module> { fn eval(&self) -> Module {
eval( eval(&mut self.ctx.borrow_mut(), self.file, Rc::clone(&self.ast)).unwrap()
&mut self.ctx.borrow_mut(),
self.src_id,
Rc::clone(&self.ast),
)
} }
fn exec(&self) -> Pass<LayoutTree> { fn exec(&self) -> LayoutTree {
exec(&mut self.ctx.borrow_mut(), &self.module.template) exec(&mut self.ctx.borrow_mut(), &self.module.template)
} }
@ -127,8 +122,8 @@ impl Case {
layout(&mut self.ctx.borrow_mut(), &self.tree) layout(&mut self.ctx.borrow_mut(), &self.tree)
} }
fn typeset(&self) -> Pass<Vec<Rc<Frame>>> { fn typeset(&self) -> Vec<Rc<Frame>> {
self.ctx.borrow_mut().typeset(self.src_id, &self.src) self.ctx.borrow_mut().typeset(self.file, &self.src).unwrap()
} }
fn pdf(&self) -> Vec<u8> { fn pdf(&self) -> Vec<u8> {

View File

@ -1,6 +1,7 @@
use iai::{black_box, main}; use iai::{black_box, main};
use typst::diag::Pass; use typst::diag::TypResult;
use typst::loading::FileId;
use typst::parse::{parse, Scanner, TokenMode, Tokens}; use typst::parse::{parse, Scanner, TokenMode, Tokens};
use typst::syntax::SyntaxTree; use typst::syntax::SyntaxTree;
@ -30,8 +31,8 @@ fn bench_tokenize() -> usize {
Tokens::new(black_box(SRC), black_box(TokenMode::Markup)).count() Tokens::new(black_box(SRC), black_box(TokenMode::Markup)).count()
} }
fn bench_parse() -> Pass<SyntaxTree> { fn bench_parse() -> TypResult<SyntaxTree> {
parse(black_box(SRC)) parse(FileId::from_raw(0), black_box(SRC))
} }
main!(bench_decode, bench_scan, bench_tokenize, bench_parse); main!(bench_decode, bench_scan, bench_tokenize, bench_parse);

View File

@ -1,113 +1,70 @@
//! Diagnostics for source code. //! Diagnostics.
//!
//! Errors are never fatal, the document will always compile and yield a layout.
use std::collections::BTreeSet;
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::loading::FileId;
use crate::syntax::Span; use crate::syntax::Span;
/// The result of some pass: Some output `T` and diagnostics. /// The result type for typesetting and all its subpasses.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub type TypResult<T> = Result<T, Box<Vec<Error>>>;
pub struct Pass<T> {
/// The output of this compilation pass.
pub output: T,
/// User diagnostics accumulated in this pass.
pub diags: DiagSet,
}
impl<T> Pass<T> { /// A result type with a string error message.
/// Create a new pass from output and diagnostics. pub type StrResult<T> = Result<T, String>;
pub fn new(output: T, diags: DiagSet) -> Self {
Self { output, diags }
}
}
/// A set of diagnostics. /// An error in a source file.
///
/// Since this is a [`BTreeSet`], there cannot be two equal (up to span)
/// diagnostics and you can quickly iterate diagnostics in source location
/// order.
pub type DiagSet = BTreeSet<Diag>;
/// A diagnostic with severity level and message.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct Diag { pub struct Error {
/// The source code location. /// The file that contains the error.
pub file: FileId,
/// The erronous location in the source code.
pub span: Span, pub span: Span,
/// How severe / important the diagnostic is. /// A diagnostic message describing the problem.
pub level: Level,
/// A message describing the diagnostic.
pub message: String, pub message: String,
} }
impl Diag { impl Error {
/// Create a new diagnostic from message and level. /// Create a new, bare error.
pub fn new(span: impl Into<Span>, level: Level, message: impl Into<String>) -> Self { pub fn new(file: FileId, span: impl Into<Span>, message: impl Into<String>) -> Self {
Self { Self {
file,
span: span.into(), span: span.into(),
level,
message: message.into(), message: message.into(),
} }
} }
}
impl Display for Diag { /// Create a boxed vector containing one error. The return value is suitable
fn fmt(&self, f: &mut Formatter) -> fmt::Result { /// as the `Err` variant of a [`TypResult`].
write!(f, "{}: {}", self.level, self.message) pub fn boxed(
file: FileId,
span: impl Into<Span>,
message: impl Into<String>,
) -> Box<Vec<Self>> {
Box::new(vec![Self::new(file, span, message)])
}
/// Partially build a vec-boxed error, returning a function that just needs
/// the message.
///
/// This is useful in to convert from [`StrResult`] to a [`TypResult`] using
/// [`map_err`](Result::map_err).
pub fn partial(
file: FileId,
span: impl Into<Span>,
) -> impl FnOnce(String) -> Box<Vec<Self>> {
move |message| Self::boxed(file, span, message)
} }
} }
/// How severe / important a diagnostic is. /// Early-return with a vec-boxed [`Error`].
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Level {
Warning,
Error,
}
impl Display for Level {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {
Self::Warning => "warning",
Self::Error => "error",
})
}
}
/// Construct a diagnostic with [`Error`](Level::Error) level.
///
/// ```
/// # use typst::error;
/// # use typst::syntax::Span;
/// # let span = Span::ZERO;
/// # let thing = "";
/// let error = error!(span, "there is an error with {}", thing);
/// ```
#[macro_export] #[macro_export]
macro_rules! error { macro_rules! bail {
($span:expr, $($tts:tt)*) => { ($file:expr, $span:expr, $message:expr $(,)?) => {
$crate::diag::Diag::new( return Err(Box::new(vec![$crate::diag::Error::new(
$span, $file, $span, $message,
$crate::diag::Level::Error, )]));
format!($($tts)*), };
)
}; ($file:expr, $span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => {
} $crate::bail!($file, $span, format!($fmt, $($arg),+));
/// Construct a diagnostic with [`Warning`](Level::Warning) level.
///
/// This works exactly like [`error!`].
#[macro_export]
macro_rules! warning {
($span:expr, $($tts:tt)*) => {
$crate::diag::Diag::new(
$span,
$crate::diag::Level::Warning,
format!($($tts)*),
)
}; };
} }

View File

@ -3,8 +3,10 @@ use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use super::{Cast, EvalContext, Value}; use super::{Cast, EvalContext, Value};
use crate::util::EcoString; use crate::diag::{Error, TypResult};
use crate::loading::FileId;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
use crate::util::EcoString;
/// An evaluatable function. /// An evaluatable function.
#[derive(Clone)] #[derive(Clone)]
@ -16,13 +18,13 @@ struct Repr<T: ?Sized> {
func: T, func: T,
} }
type Func = dyn Fn(&mut EvalContext, &mut FuncArgs) -> Value; type Func = dyn Fn(&mut EvalContext, &mut FuncArgs) -> TypResult<Value>;
impl Function { impl Function {
/// Create a new function from a rust closure. /// Create a new function from a rust closure.
pub fn new<F>(name: Option<EcoString>, func: F) -> Self pub fn new<F>(name: Option<EcoString>, func: F) -> Self
where where
F: Fn(&mut EvalContext, &mut FuncArgs) -> Value + 'static, F: Fn(&mut EvalContext, &mut FuncArgs) -> TypResult<Value> + 'static,
{ {
Self(Rc::new(Repr { name, func })) Self(Rc::new(Repr { name, func }))
} }
@ -57,6 +59,8 @@ impl PartialEq for Function {
/// Evaluated arguments to a function. /// Evaluated arguments to a function.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct FuncArgs { pub struct FuncArgs {
/// The file in which the function was called.
pub file: FileId,
/// The span of the whole argument list. /// The span of the whole argument list.
pub span: Span, pub span: Span,
/// The positional arguments. /// The positional arguments.
@ -80,32 +84,30 @@ impl FuncArgs {
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
{ {
(0 .. self.items.len()).find_map(|index| { for (i, slot) in self.items.iter().enumerate() {
let slot = self.items.get_mut(index)?;
if slot.name.is_none() { if slot.name.is_none() {
if T::is(&slot.value) { if T::is(&slot.value) {
let value = self.items.remove(index).value; let value = self.items.remove(i).value;
return T::cast(value).ok(); return T::cast(value).ok();
} }
} }
None }
}) None
} }
/// Find and consume the first castable positional argument, producing a /// Find and consume the first castable positional argument, returning a
/// `missing argument: {what}` error if no match was found. /// `missing argument: {what}` error if no match was found.
pub fn expect<T>(&mut self, ctx: &mut EvalContext, what: &str) -> Option<T> pub fn expect<T>(&mut self, what: &str) -> TypResult<T>
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
{ {
let found = self.eat(); match self.eat() {
if found.is_none() { Some(found) => Ok(found),
ctx.diag(error!(self.span, "missing argument: {}", what)); None => bail!(self.file, self.span, "missing argument: {}", what),
} }
found
} }
/// Find, consume and collect all castable positional arguments. /// Find and consume all castable positional arguments.
pub fn all<T>(&mut self) -> impl Iterator<Item = T> + '_ pub fn all<T>(&mut self) -> impl Iterator<Item = T> + '_
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
@ -113,35 +115,34 @@ impl FuncArgs {
std::iter::from_fn(move || self.eat()) std::iter::from_fn(move || self.eat())
} }
/// Cast and remove the value for the given named argument, producing an /// Cast and remove the value for the given named argument, returning an
/// error if the conversion fails. /// error if the conversion fails.
pub fn named<T>(&mut self, ctx: &mut EvalContext, name: &str) -> Option<T> pub fn named<T>(&mut self, name: &str) -> TypResult<Option<T>>
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
{ {
let index = self let index = match self
.items .items
.iter() .iter()
.position(|arg| arg.name.as_ref().map_or(false, |other| other == name))?; .filter_map(|arg| arg.name.as_deref())
.position(|other| name == other)
{
Some(index) => index,
None => return Ok(None),
};
let value = self.items.remove(index).value; let value = self.items.remove(index).value;
let span = value.span; let span = value.span;
match T::cast(value) { T::cast(value).map(Some).map_err(Error::partial(self.file, span))
Ok(t) => Some(t),
Err(msg) => {
ctx.diag(error!(span, "{}", msg));
None
}
}
} }
/// Produce "unexpected argument" errors for all remaining arguments. /// Return an "unexpected argument" error if there is any remaining
pub fn finish(self, ctx: &mut EvalContext) { /// argument.
for arg in &self.items { pub fn finish(self) -> TypResult<()> {
if arg.value.v != Value::Error { if let Some(arg) = self.items.first() {
ctx.diag(error!(arg.span, "unexpected argument")); bail!(self.file, arg.span, "unexpected argument");
}
} }
Ok(())
} }
} }

View File

@ -25,22 +25,21 @@ use std::mem;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use crate::diag::{Diag, DiagSet, Pass}; use crate::diag::{Error, StrResult, TypResult};
use crate::util::EcoString;
use crate::geom::{Angle, Fractional, Length, Relative}; use crate::geom::{Angle, Fractional, Length, Relative};
use crate::image::ImageCache; use crate::image::ImageCache;
use crate::loading::{FileId, Loader}; use crate::loading::{FileId, Loader};
use crate::parse::parse; use crate::parse::parse;
use crate::syntax::visit::Visit; use crate::syntax::visit::Visit;
use crate::syntax::*; use crate::syntax::*;
use crate::util::EcoString;
use crate::Context; use crate::Context;
/// Evaluate a parsed source file into a module. /// Evaluate a parsed source file into a module.
pub fn eval(ctx: &mut Context, file: FileId, ast: Rc<SyntaxTree>) -> Pass<Module> { pub fn eval(ctx: &mut Context, file: FileId, ast: Rc<SyntaxTree>) -> TypResult<Module> {
let mut ctx = EvalContext::new(ctx, file); let mut ctx = EvalContext::new(ctx, file);
let template = ast.eval(&mut ctx); let template = ast.eval(&mut ctx)?;
let module = Module { scope: ctx.scopes.top, template }; Ok(Module { scope: ctx.scopes.top, template })
Pass::new(module, ctx.diags)
} }
/// Caches evaluated modules. /// Caches evaluated modules.
@ -61,7 +60,7 @@ pub trait Eval {
type Output; type Output;
/// Evaluate the expression to the output value. /// Evaluate the expression to the output value.
fn eval(&self, ctx: &mut EvalContext) -> Self::Output; fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>;
} }
/// The context for evaluation. /// The context for evaluation.
@ -74,10 +73,12 @@ pub struct EvalContext<'a> {
pub modules: &'a mut ModuleCache, pub modules: &'a mut ModuleCache,
/// The active scopes. /// The active scopes.
pub scopes: Scopes<'a>, pub scopes: Scopes<'a>,
/// Evaluation diagnostics. /// The currently evaluated file.
pub diags: DiagSet, pub file: FileId,
/// The stack of imported files that led to evaluation of the current file. /// The stack of imported files that led to evaluation of the current file.
pub route: Vec<FileId>, pub route: Vec<FileId>,
/// The expression map for the currently built template.
pub map: ExprMap,
} }
impl<'a> EvalContext<'a> { impl<'a> EvalContext<'a> {
@ -88,141 +89,123 @@ impl<'a> EvalContext<'a> {
images: &mut ctx.images, images: &mut ctx.images,
modules: &mut ctx.modules, modules: &mut ctx.modules,
scopes: Scopes::new(Some(&ctx.std)), scopes: Scopes::new(Some(&ctx.std)),
diags: DiagSet::new(), file,
route: vec![file], route: vec![],
map: ExprMap::new(),
} }
} }
/// Resolve a path relative to the current file. /// Resolve a path relative to the current file.
/// ///
/// Generates an error if the file is not found. /// Returns an error if the file is not found.
pub fn resolve(&mut self, path: &str, span: Span) -> Option<FileId> { pub fn resolve(&mut self, path: &str, span: Span) -> TypResult<FileId> {
let base = *self.route.last()?; self.loader
self.loader.resolve_from(base, Path::new(path)).ok().or_else(|| { .resolve_from(self.file, Path::new(path))
self.diag(error!(span, "file not found")); .map_err(|_| Error::boxed(self.file, span, "file not found"))
None
})
} }
/// Process an import of a module relative to the current location. /// Process an import of a module relative to the current location.
pub fn import(&mut self, path: &str, span: Span) -> Option<FileId> { pub fn import(&mut self, path: &str, span: Span) -> TypResult<FileId> {
let id = self.resolve(path, span)?; let id = self.resolve(path, span)?;
// Prevent cyclic importing. // Prevent cyclic importing.
if self.route.contains(&id) { if self.file == id || self.route.contains(&id) {
self.diag(error!(span, "cyclic import")); bail!(self.file, span, "cyclic import");
return None;
} }
// Check whether the module was already loaded. // Check whether the module was already loaded.
if self.modules.get(&id).is_some() { if self.modules.get(&id).is_some() {
return Some(id); return Ok(id);
} }
let buffer = self.loader.load_file(id).ok().or_else(|| { // Load the source file.
self.diag(error!(span, "failed to load file")); let buffer = self
None .loader
})?; .load_file(id)
.map_err(|_| Error::boxed(self.file, span, "failed to load file"))?;
let string = std::str::from_utf8(&buffer).ok().or_else(|| { // Decode UTF-8.
self.diag(error!(span, "file is not valid utf-8")); let string = std::str::from_utf8(&buffer)
None .map_err(|_| Error::boxed(self.file, span, "file is not valid utf-8"))?;
})?;
// Parse the file. // Parse the file.
let parsed = parse(string); let ast = parse(id, string)?;
// Prepare the new context. // Prepare the new context.
let new_scopes = Scopes::new(self.scopes.base); let new_scopes = Scopes::new(self.scopes.base);
let old_scopes = 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); self.route.push(self.file);
self.route.push(id); self.file = id;
// Evaluate the module. // Evaluate the module.
let ast = Rc::new(parsed.output); let template = Rc::new(ast).eval(self)?;
let template = ast.eval(self);
// Restore the old context. // Restore the old context.
let new_scopes = 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.file = self.route.pop().unwrap();
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. // Save the evaluated module.
let module = Module { scope: new_scopes.top, template }; let module = Module { scope: new_scopes.top, template };
self.modules.insert(id, module); self.modules.insert(id, module);
Some(id) Ok(id)
}
/// Add a diagnostic.
pub fn diag(&mut self, diag: Diag) {
self.diags.insert(diag);
}
/// Cast a value to a type and diagnose a possible error / warning.
pub fn cast<T>(&mut self, value: Value, span: Span) -> Option<T>
where
T: Cast<Value>,
{
if value == Value::Error {
return None;
}
match T::cast(value) {
Ok(value) => Some(value),
Err(msg) => {
self.diag(error!(span, "{}", msg));
None
}
}
}
/// Join with another value.
pub fn join(&mut self, lhs: Value, rhs: Value, span: Span) -> Value {
let (a, b) = (lhs.type_name(), rhs.type_name());
match ops::join(lhs, rhs) {
Ok(joined) => joined,
Err(prev) => {
self.diag(error!(span, "cannot join {} with {}", a, b));
prev
}
}
} }
} }
impl Eval for Rc<SyntaxTree> { impl Eval for Rc<SyntaxTree> {
type Output = Template; type Output = Template;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
struct ExprVisitor<'a, 'b> { trait Walk {
ctx: &'a mut EvalContext<'b>, fn walk(&self, ctx: &mut EvalContext) -> TypResult<()>;
map: ExprMap,
} }
impl<'ast> Visit<'ast> for ExprVisitor<'_, '_> { impl Walk for SyntaxTree {
fn visit_expr(&mut self, node: &'ast Expr) { fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
self.map.insert(node as *const _, node.eval(self.ctx)); for node in self.iter() {
node.walk(ctx)?;
}
Ok(())
} }
} }
let mut visitor = ExprVisitor { ctx, map: ExprMap::new() }; impl Walk for SyntaxNode {
visitor.visit_tree(self); fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> {
match self {
Self::Text(_) => {}
Self::Space => {}
Self::Linebreak(_) => {}
Self::Parbreak(_) => {}
Self::Strong(_) => {}
Self::Emph(_) => {}
Self::Raw(_) => {}
Self::Heading(n) => n.body.walk(ctx)?,
Self::List(n) => n.body.walk(ctx)?,
Self::Enum(n) => n.body.walk(ctx)?,
Self::Expr(n) => {
let value = n.eval(ctx)?;
ctx.map.insert(n as *const _, value);
}
}
Ok(())
}
}
TemplateTree { tree: Rc::clone(self), map: visitor.map }.into() let map = {
let prev = mem::take(&mut ctx.map);
self.walk(ctx)?;
mem::replace(&mut ctx.map, prev)
};
Ok(TemplateTree { tree: Rc::clone(self), map }.into())
} }
} }
impl Eval for Expr { impl Eval for Expr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
match *self { Ok(match *self {
Self::None(_) => Value::None, Self::None(_) => Value::None,
Self::Auto(_) => Value::Auto, Self::Auto(_) => Value::Auto,
Self::Bool(_, v) => Value::Bool(v), Self::Bool(_, v) => Value::Bool(v),
@ -235,35 +218,32 @@ impl Eval for Expr {
Self::Str(_, ref v) => Value::Str(v.clone()), Self::Str(_, ref v) => Value::Str(v.clone()),
Self::Ident(ref v) => match ctx.scopes.get(&v) { Self::Ident(ref v) => match ctx.scopes.get(&v) {
Some(slot) => slot.borrow().clone(), Some(slot) => slot.borrow().clone(),
None => { None => bail!(ctx.file, v.span, "unknown variable"),
ctx.diag(error!(v.span, "unknown variable"));
Value::Error
}
}, },
Self::Array(ref v) => Value::Array(v.eval(ctx)), Self::Array(ref v) => Value::Array(v.eval(ctx)?),
Self::Dict(ref v) => Value::Dict(v.eval(ctx)), Self::Dict(ref v) => Value::Dict(v.eval(ctx)?),
Self::Template(ref v) => Value::Template(v.eval(ctx)), Self::Template(ref v) => Value::Template(v.eval(ctx)?),
Self::Group(ref v) => v.eval(ctx), Self::Group(ref v) => v.eval(ctx)?,
Self::Block(ref v) => v.eval(ctx), Self::Block(ref v) => v.eval(ctx)?,
Self::Call(ref v) => v.eval(ctx), Self::Call(ref v) => v.eval(ctx)?,
Self::Closure(ref v) => v.eval(ctx), Self::Closure(ref v) => v.eval(ctx)?,
Self::With(ref v) => v.eval(ctx), Self::With(ref v) => v.eval(ctx)?,
Self::Unary(ref v) => v.eval(ctx), Self::Unary(ref v) => v.eval(ctx)?,
Self::Binary(ref v) => v.eval(ctx), Self::Binary(ref v) => v.eval(ctx)?,
Self::Let(ref v) => v.eval(ctx), Self::Let(ref v) => v.eval(ctx)?,
Self::If(ref v) => v.eval(ctx), Self::If(ref v) => v.eval(ctx)?,
Self::While(ref v) => v.eval(ctx), Self::While(ref v) => v.eval(ctx)?,
Self::For(ref v) => v.eval(ctx), Self::For(ref v) => v.eval(ctx)?,
Self::Import(ref v) => v.eval(ctx), Self::Import(ref v) => v.eval(ctx)?,
Self::Include(ref v) => v.eval(ctx), Self::Include(ref v) => v.eval(ctx)?,
} })
} }
} }
impl Eval for ArrayExpr { impl Eval for ArrayExpr {
type Output = Array; type Output = Array;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
self.items.iter().map(|expr| expr.eval(ctx)).collect() self.items.iter().map(|expr| expr.eval(ctx)).collect()
} }
} }
@ -271,10 +251,10 @@ impl Eval for ArrayExpr {
impl Eval for DictExpr { impl Eval for DictExpr {
type Output = Dict; type Output = Dict;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
self.items self.items
.iter() .iter()
.map(|Named { name, expr }| (name.string.clone(), expr.eval(ctx))) .map(|Named { name, expr }| Ok((name.string.clone(), expr.eval(ctx)?)))
.collect() .collect()
} }
} }
@ -282,7 +262,7 @@ impl Eval for DictExpr {
impl Eval for TemplateExpr { impl Eval for TemplateExpr {
type Output = Template; type Output = Template;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
self.tree.eval(ctx) self.tree.eval(ctx)
} }
} }
@ -290,7 +270,7 @@ impl Eval for TemplateExpr {
impl Eval for GroupExpr { impl Eval for GroupExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
self.expr.eval(ctx) self.expr.eval(ctx)
} }
} }
@ -298,58 +278,44 @@ impl Eval for GroupExpr {
impl Eval for BlockExpr { impl Eval for BlockExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
if self.scoping { if self.scoping {
ctx.scopes.enter(); ctx.scopes.enter();
} }
let mut output = Value::None; let mut output = Value::None;
for expr in &self.exprs { for expr in &self.exprs {
let value = expr.eval(ctx); let value = expr.eval(ctx)?;
output = ctx.join(output, value, expr.span()); output = ops::join(output, value)
.map_err(Error::partial(ctx.file, expr.span()))?;
} }
if self.scoping { if self.scoping {
ctx.scopes.exit(); ctx.scopes.exit();
} }
output Ok(output)
} }
} }
impl Eval for UnaryExpr { impl Eval for UnaryExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let value = self.expr.eval(ctx); let value = self.expr.eval(ctx)?;
if value == Value::Error { let result = match self.op {
return Value::Error;
}
let ty = value.type_name();
let out = match self.op {
UnOp::Pos => ops::pos(value), UnOp::Pos => ops::pos(value),
UnOp::Neg => ops::neg(value), UnOp::Neg => ops::neg(value),
UnOp::Not => ops::not(value), UnOp::Not => ops::not(value),
}; };
result.map_err(Error::partial(ctx.file, self.span))
if out == Value::Error {
ctx.diag(error!(
self.span,
"cannot apply '{}' to {}",
self.op.as_str(),
ty,
));
}
out
} }
} }
impl Eval for BinaryExpr { impl Eval for BinaryExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
match self.op { match self.op {
BinOp::Add => self.apply(ctx, ops::add), BinOp::Add => self.apply(ctx, ops::add),
BinOp::Sub => self.apply(ctx, ops::sub), BinOp::Sub => self.apply(ctx, ops::sub),
@ -363,7 +329,7 @@ impl Eval for BinaryExpr {
BinOp::Leq => self.apply(ctx, ops::leq), BinOp::Leq => self.apply(ctx, ops::leq),
BinOp::Gt => self.apply(ctx, ops::gt), BinOp::Gt => self.apply(ctx, ops::gt),
BinOp::Geq => self.apply(ctx, ops::geq), BinOp::Geq => self.apply(ctx, ops::geq),
BinOp::Assign => self.assign(ctx, |_, b| b), BinOp::Assign => self.assign(ctx, |_, b| Ok(b)),
BinOp::AddAssign => self.assign(ctx, ops::add), BinOp::AddAssign => self.assign(ctx, ops::add),
BinOp::SubAssign => self.assign(ctx, ops::sub), BinOp::SubAssign => self.assign(ctx, ops::sub),
BinOp::MulAssign => self.assign(ctx, ops::mul), BinOp::MulAssign => self.assign(ctx, ops::mul),
@ -375,131 +341,110 @@ impl Eval for BinaryExpr {
impl BinaryExpr { impl BinaryExpr {
/// Apply a basic binary operation. /// Apply a basic binary operation.
fn apply<F>(&self, ctx: &mut EvalContext, op: F) -> Value fn apply<F>(&self, ctx: &mut EvalContext, op: F) -> TypResult<Value>
where where
F: FnOnce(Value, Value) -> Value, F: FnOnce(Value, Value) -> StrResult<Value>,
{ {
let lhs = self.lhs.eval(ctx)?;
// Short-circuit boolean operations. // Short-circuit boolean operations.
let lhs = self.lhs.eval(ctx); if (self.op == BinOp::And && lhs == Value::Bool(false))
match (self.op, &lhs) { || (self.op == BinOp::Or && lhs == Value::Bool(true))
(BinOp::And, Value::Bool(false)) => return lhs, {
(BinOp::Or, Value::Bool(true)) => return lhs, return Ok(lhs);
_ => {}
} }
let rhs = self.rhs.eval(ctx); let rhs = self.rhs.eval(ctx)?;
if lhs == Value::Error || rhs == Value::Error { op(lhs, rhs).map_err(Error::partial(ctx.file, self.span))
return Value::Error;
}
// Save type names before we consume the values in case of error.
let types = (lhs.type_name(), rhs.type_name());
let out = op(lhs, rhs);
if out == Value::Error {
self.error(ctx, types);
}
out
} }
/// Apply an assignment operation. /// Apply an assignment operation.
fn assign<F>(&self, ctx: &mut EvalContext, op: F) -> Value fn assign<F>(&self, ctx: &mut EvalContext, op: F) -> TypResult<Value>
where where
F: FnOnce(Value, Value) -> Value, F: FnOnce(Value, Value) -> StrResult<Value>,
{ {
let lspan = self.lhs.span();
let slot = if let Expr::Ident(id) = self.lhs.as_ref() { let slot = if let Expr::Ident(id) = self.lhs.as_ref() {
match ctx.scopes.get(id) { match ctx.scopes.get(id) {
Some(slot) => Rc::clone(slot), Some(slot) => Rc::clone(slot),
None => { None => bail!(ctx.file, lspan, "unknown variable"),
ctx.diag(error!(self.lhs.span(), "unknown variable"));
return Value::Error;
}
} }
} else { } else {
ctx.diag(error!(self.lhs.span(), "cannot assign to this expression")); bail!(ctx.file, lspan, "cannot assign to this expression",);
return Value::Error;
}; };
let rhs = self.rhs.eval(ctx); let rhs = self.rhs.eval(ctx)?;
let mut mutable = match slot.try_borrow_mut() { let mut mutable = match slot.try_borrow_mut() {
Ok(mutable) => mutable, Ok(mutable) => mutable,
Err(_) => { Err(_) => {
ctx.diag(error!(self.lhs.span(), "cannot assign to a constant")); bail!(ctx.file, lspan, "cannot assign to a constant",);
return Value::Error;
} }
}; };
let lhs = mem::take(&mut *mutable); let lhs = mem::take(&mut *mutable);
let types = (lhs.type_name(), rhs.type_name()); *mutable = op(lhs, rhs).map_err(Error::partial(ctx.file, self.span))?;
*mutable = op(lhs, rhs);
if *mutable == Value::Error { Ok(Value::None)
self.error(ctx, types);
return Value::Error;
}
Value::None
}
fn error(&self, ctx: &mut EvalContext, (a, b): (&str, &str)) {
ctx.diag(error!(self.span, "{}", match self.op {
BinOp::Add => format!("cannot add {} and {}", a, b),
BinOp::Sub => format!("cannot subtract {1} from {0}", a, b),
BinOp::Mul => format!("cannot multiply {} with {}", a, b),
BinOp::Div => format!("cannot divide {} by {}", a, b),
_ => format!("cannot apply '{}' to {} and {}", self.op.as_str(), a, b),
}));
} }
} }
impl Eval for CallExpr { impl Eval for CallExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let callee = self.callee.eval(ctx); let callee = self
if let Some(func) = ctx.cast::<Function>(callee, self.callee.span()) { .callee
let mut args = self.args.eval(ctx); .eval(ctx)?
let returned = func(ctx, &mut args); .cast::<Function>()
args.finish(ctx); .map_err(Error::partial(ctx.file, self.callee.span()))?;
returned
} else { let mut args = self.args.eval(ctx)?;
Value::Error let returned = callee(ctx, &mut args)?;
} args.finish()?;
Ok(returned)
} }
} }
impl Eval for CallArgs { impl Eval for CallArgs {
type Output = FuncArgs; type Output = FuncArgs;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let items = self.items.iter().map(|arg| arg.eval(ctx)).collect(); Ok(FuncArgs {
FuncArgs { span: self.span, items } file: ctx.file,
span: self.span,
items: self
.items
.iter()
.map(|arg| arg.eval(ctx))
.collect::<TypResult<Vec<_>>>()?,
})
} }
} }
impl Eval for CallArg { impl Eval for CallArg {
type Output = FuncArg; type Output = FuncArg;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
match self { Ok(match self {
Self::Pos(expr) => FuncArg { Self::Pos(expr) => FuncArg {
span: self.span(), span: self.span(),
name: None, name: None,
value: Spanned::new(expr.eval(ctx), expr.span()), value: Spanned::new(expr.eval(ctx)?, expr.span()),
}, },
Self::Named(Named { name, expr }) => FuncArg { Self::Named(Named { name, expr }) => FuncArg {
span: self.span(), span: self.span(),
name: Some(name.string.clone()), name: Some(name.string.clone()),
value: Spanned::new(expr.eval(ctx), expr.span()), value: Spanned::new(expr.eval(ctx)?, expr.span()),
}, },
} })
} }
} }
impl Eval for ClosureExpr { impl Eval for ClosureExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let params = Rc::clone(&self.params); let params = Rc::clone(&self.params);
let body = Rc::clone(&self.body); let body = Rc::clone(&self.body);
@ -511,86 +456,92 @@ impl Eval for ClosureExpr {
}; };
let name = self.name.as_ref().map(|name| name.string.clone()); let name = self.name.as_ref().map(|name| name.string.clone());
Value::Func(Function::new(name, move |ctx, args| { let func = Function::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 = 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() {
// Set the parameter to `none` if the argument is missing. let value = args.expect::<Value>(param.as_str())?;
let value = args.expect::<Value>(ctx, param.as_str()).unwrap_or_default();
ctx.scopes.def_mut(param.as_str(), value); ctx.scopes.def_mut(param.as_str(), value);
} }
let value = body.eval(ctx); let result = body.eval(ctx);
ctx.scopes = prev; ctx.scopes = prev;
value result
})) });
Ok(Value::Func(func))
} }
} }
impl Eval for WithExpr { impl Eval for WithExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let callee = self.callee.eval(ctx); let callee = self
if let Some(func) = ctx.cast::<Function>(callee, self.callee.span()) { .callee
let applied = self.args.eval(ctx); .eval(ctx)?
let name = func.name().cloned(); .cast::<Function>()
Value::Func(Function::new(name, move |ctx, args| { .map_err(Error::partial(ctx.file, self.callee.span()))?;
// Remove named arguments that were overridden.
let kept: Vec<_> = applied
.items
.iter()
.filter(|arg| {
arg.name.is_none()
|| args.items.iter().all(|other| arg.name != other.name)
})
.cloned()
.collect();
// Preprend the applied arguments so that the positional arguments let applied = self.args.eval(ctx)?;
// are in the right order.
args.items.splice(.. 0, kept);
// Call the original function. let name = callee.name().cloned();
func(ctx, args) let func = Function::new(name, move |ctx, args| {
})) // Remove named arguments that were overridden.
} else { let kept: Vec<_> = applied
Value::Error .items
} .iter()
.filter(|arg| {
arg.name.is_none()
|| args.items.iter().all(|other| arg.name != other.name)
})
.cloned()
.collect();
// Preprend the applied arguments so that the positional arguments
// are in the right order.
args.items.splice(.. 0, kept);
// Call the original function.
callee(ctx, args)
});
Ok(Value::Func(func))
} }
} }
impl Eval for LetExpr { impl Eval for LetExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let value = match &self.init { let value = match &self.init {
Some(expr) => expr.eval(ctx), Some(expr) => expr.eval(ctx)?,
None => Value::None, None => Value::None,
}; };
ctx.scopes.def_mut(self.binding.as_str(), value); ctx.scopes.def_mut(self.binding.as_str(), value);
Value::None Ok(Value::None)
} }
} }
impl Eval for IfExpr { impl Eval for IfExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let condition = self.condition.eval(ctx); let condition = self
if let Some(condition) = ctx.cast(condition, self.condition.span()) { .condition
if condition { .eval(ctx)?
self.if_body.eval(ctx) .cast::<bool>()
} else if let Some(else_body) = &self.else_body { .map_err(Error::partial(ctx.file, self.condition.span()))?;
else_body.eval(ctx)
} else { if condition {
Value::None self.if_body.eval(ctx)
} } else if let Some(else_body) = &self.else_body {
else_body.eval(ctx)
} else { } else {
Value::Error Ok(Value::None)
} }
} }
} }
@ -598,28 +549,28 @@ impl Eval for IfExpr {
impl Eval for WhileExpr { impl Eval for WhileExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let mut output = Value::None; let mut output = Value::None;
loop {
let condition = self.condition.eval(ctx); while self
if let Some(condition) = ctx.cast(condition, self.condition.span()) { .condition
if condition { .eval(ctx)?
let value = self.body.eval(ctx); .cast::<bool>()
output = ctx.join(output, value, self.body.span()); .map_err(Error::partial(ctx.file, self.condition.span()))?
} else { {
return output; let value = self.body.eval(ctx)?;
} output = ops::join(output, value)
} else { .map_err(Error::partial(ctx.file, self.body.span()))?;
return Value::Error;
}
} }
Ok(output)
} }
} }
impl Eval for ForExpr { impl Eval for ForExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
macro_rules! iter { macro_rules! iter {
(for ($($binding:ident => $value:ident),*) in $iter:expr) => {{ (for ($($binding:ident => $value:ident),*) in $iter:expr) => {{
let mut output = Value::None; let mut output = Value::None;
@ -629,17 +580,18 @@ impl Eval for ForExpr {
for ($($value),*) in $iter { for ($($value),*) in $iter {
$(ctx.scopes.def_mut($binding.as_str(), $value);)* $(ctx.scopes.def_mut($binding.as_str(), $value);)*
let value = self.body.eval(ctx); let value = self.body.eval(ctx)?;
output = ctx.join(output, value, self.body.span()); output = ops::join(output, value)
.map_err(Error::partial(ctx.file, self.body.span()))?;
} }
ctx.scopes.exit(); ctx.scopes.exit();
output Ok(output)
}}; }};
} }
let iter = self.iter.eval(ctx); let iter = self.iter.eval(ctx)?;
match (self.pattern.clone(), iter) { match (&self.pattern, iter) {
(ForPattern::Value(v), Value::Str(string)) => { (ForPattern::Value(v), Value::Str(string)) => {
iter!(for (v => value) in string.chars().map(|c| Value::Str(c.into()))) iter!(for (v => value) in string.chars().map(|c| Value::Str(c.into())))
} }
@ -655,22 +607,15 @@ impl Eval for ForExpr {
(ForPattern::KeyValue(k, v), Value::Dict(dict)) => { (ForPattern::KeyValue(k, v), Value::Dict(dict)) => {
iter!(for (k => key, v => value) in dict.into_iter()) iter!(for (k => key, v => value) in dict.into_iter())
} }
(ForPattern::KeyValue(_, _), Value::Str(_)) => { (ForPattern::KeyValue(_, _), Value::Str(_)) => {
ctx.diag(error!(self.pattern.span(), "mismatched pattern")); bail!(ctx.file, self.pattern.span(), "mismatched pattern");
Value::Error
}
(_, iter) => {
if iter != Value::Error {
ctx.diag(error!(
self.iter.span(),
"cannot loop over {}",
iter.type_name(),
));
}
Value::Error
} }
(_, iter) => bail!(
ctx.file,
self.iter.span(),
"cannot loop over {}",
iter.type_name(),
),
} }
} }
} }
@ -678,50 +623,50 @@ impl Eval for ForExpr {
impl Eval for ImportExpr { impl Eval for ImportExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let path = self.path.eval(ctx); let path = self
if let Some(path) = ctx.cast::<EcoString>(path, self.path.span()) { .path
if let Some(hash) = ctx.import(&path, self.path.span()) { .eval(ctx)?
let mut module = &ctx.modules[&hash]; .cast::<EcoString>()
match &self.imports { .map_err(Error::partial(ctx.file, self.path.span()))?;
Imports::Wildcard => {
for (var, slot) in module.scope.iter() { let id = ctx.import(&path, self.path.span())?;
let value = slot.borrow().clone(); let module = &ctx.modules[&id];
ctx.scopes.def_mut(var, value);
} match &self.imports {
} Imports::Wildcard => {
Imports::Idents(idents) => { for (var, slot) in module.scope.iter() {
for ident in idents { ctx.scopes.def_mut(var, slot.borrow().clone());
if let Some(slot) = module.scope.get(&ident) { }
let value = slot.borrow().clone(); }
ctx.scopes.def_mut(ident.as_str(), value); Imports::Idents(idents) => {
} else { for ident in idents {
ctx.diag(error!(ident.span, "unresolved import")); if let Some(slot) = module.scope.get(&ident) {
module = &ctx.modules[&hash]; ctx.scopes.def_mut(ident.as_str(), slot.borrow().clone());
} } else {
} bail!(ctx.file, ident.span, "unresolved import");
} }
} }
return Value::None;
} }
} }
Value::Error Ok(Value::None)
} }
} }
impl Eval for IncludeExpr { impl Eval for IncludeExpr {
type Output = Value; type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> Self::Output { fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
let path = self.path.eval(ctx); let path = self
if let Some(path) = ctx.cast::<EcoString>(path, self.path.span()) { .path
if let Some(hash) = ctx.import(&path, self.path.span()) { .eval(ctx)?
return Value::Template(ctx.modules[&hash].template.clone()); .cast::<EcoString>()
} .map_err(Error::partial(ctx.file, self.path.span()))?;
}
Value::Error let id = ctx.import(&path, self.path.span())?;
let module = &ctx.modules[&id];
Ok(Value::Template(module.template.clone()))
} }
} }

View File

@ -1,30 +1,34 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use super::Value; use super::Value;
use crate::diag::StrResult;
use Value::*; use Value::*;
/// Bail with a type mismatch error.
macro_rules! mismatch {
($fmt:expr, $($value:expr),* $(,)?) => {
return Err(format!($fmt, $($value.type_name()),*));
};
}
/// Join a value with another value. /// Join a value with another value.
pub fn join(lhs: Value, rhs: Value) -> Result<Value, Value> { pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(match (lhs, rhs) { Ok(match (lhs, rhs) {
(_, Error) => Error,
(Error, _) => Error,
(a, None) => a, (a, None) => a,
(None, b) => b, (None, b) => b,
(Str(a), Str(b)) => Str(a + b), (Str(a), Str(b)) => Str(a + b),
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Template(a), Template(b)) => Template(a + b), (Template(a), Template(b)) => Template(a + b),
(Template(a), Str(b)) => Template(a + b), (Template(a), Str(b)) => Template(a + b),
(Str(a), Template(b)) => Template(a + b), (Str(a), Template(b)) => Template(a + b),
(a, b) => mismatch!("cannot join {} with {}", a, b),
(lhs, _) => return Err(lhs),
}) })
} }
/// Apply the plus operator to a value. /// Apply the plus operator to a value.
pub fn pos(value: Value) -> Value { pub fn pos(value: Value) -> StrResult<Value> {
match value { Ok(match value {
Int(v) => Int(v), Int(v) => Int(v),
Float(v) => Float(v), Float(v) => Float(v),
Length(v) => Length(v), Length(v) => Length(v),
@ -32,13 +36,13 @@ pub fn pos(value: Value) -> Value {
Relative(v) => Relative(v), Relative(v) => Relative(v),
Linear(v) => Linear(v), Linear(v) => Linear(v),
Fractional(v) => Fractional(v), Fractional(v) => Fractional(v),
_ => Error, v => mismatch!("cannot apply '+' to {}", v),
} })
} }
/// Compute the negation of a value. /// Compute the negation of a value.
pub fn neg(value: Value) -> Value { pub fn neg(value: Value) -> StrResult<Value> {
match value { Ok(match value {
Int(v) => Int(-v), Int(v) => Int(-v),
Float(v) => Float(-v), Float(v) => Float(-v),
Length(v) => Length(-v), Length(v) => Length(-v),
@ -46,13 +50,13 @@ pub fn neg(value: Value) -> Value {
Relative(v) => Relative(-v), Relative(v) => Relative(-v),
Linear(v) => Linear(-v), Linear(v) => Linear(-v),
Fractional(v) => Fractional(-v), Fractional(v) => Fractional(-v),
_ => Error, v => mismatch!("cannot apply '-' to {}", v),
} })
} }
/// Compute the sum of two values. /// Compute the sum of two values.
pub fn add(lhs: Value, rhs: Value) -> Value { pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) { Ok(match (lhs, rhs) {
(Int(a), Int(b)) => Int(a + b), (Int(a), Int(b)) => Int(a + b),
(Int(a), Float(b)) => Float(a as f64 + b), (Int(a), Float(b)) => Float(a as f64 + b),
(Float(a), Int(b)) => Float(a + b as f64), (Float(a), Int(b)) => Float(a + b as f64),
@ -81,13 +85,13 @@ pub fn add(lhs: Value, rhs: Value) -> Value {
(Template(a), Str(b)) => Template(a + b), (Template(a), Str(b)) => Template(a + b),
(Str(a), Template(b)) => Template(a + b), (Str(a), Template(b)) => Template(a + b),
_ => Error, (a, b) => mismatch!("cannot add {} and {}", a, b),
} })
} }
/// Compute the difference of two values. /// Compute the difference of two values.
pub fn sub(lhs: Value, rhs: Value) -> Value { pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) { Ok(match (lhs, rhs) {
(Int(a), Int(b)) => Int(a - b), (Int(a), Int(b)) => Int(a - b),
(Int(a), Float(b)) => Float(a as f64 - b), (Int(a), Float(b)) => Float(a as f64 - b),
(Float(a), Int(b)) => Float(a - b as f64), (Float(a), Int(b)) => Float(a - b as f64),
@ -109,13 +113,13 @@ pub fn sub(lhs: Value, rhs: Value) -> Value {
(Fractional(a), Fractional(b)) => Fractional(a - b), (Fractional(a), Fractional(b)) => Fractional(a - b),
_ => Error, (a, b) => mismatch!("cannot subtract {1} from {0}", a, b),
} })
} }
/// Compute the product of two values. /// Compute the product of two values.
pub fn mul(lhs: Value, rhs: Value) -> Value { pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) { Ok(match (lhs, rhs) {
(Int(a), Int(b)) => Int(a * b), (Int(a), Int(b)) => Int(a * b),
(Int(a), Float(b)) => Float(a as f64 * b), (Int(a), Float(b)) => Float(a as f64 * b),
(Float(a), Int(b)) => Float(a * b as f64), (Float(a), Int(b)) => Float(a * b as f64),
@ -150,14 +154,16 @@ pub fn mul(lhs: Value, rhs: Value) -> Value {
(Int(a), Str(b)) => Str(b.repeat(a.max(0) as usize)), (Int(a), Str(b)) => Str(b.repeat(a.max(0) as usize)),
(Array(a), Int(b)) => Array(a.repeat(b.max(0) as usize)), (Array(a), Int(b)) => Array(a.repeat(b.max(0) as usize)),
(Int(a), Array(b)) => Array(b.repeat(a.max(0) as usize)), (Int(a), Array(b)) => Array(b.repeat(a.max(0) as usize)),
(Template(a), Int(b)) => Template(a.repeat(b.max(0) as usize)),
(Int(a), Template(b)) => Template(b.repeat(a.max(0) as usize)),
_ => Error, (a, b) => mismatch!("cannot multiply {} with {}", a, b),
} })
} }
/// Compute the quotient of two values. /// Compute the quotient of two values.
pub fn div(lhs: Value, rhs: Value) -> Value { pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) { Ok(match (lhs, rhs) {
(Int(a), Int(b)) => Float(a as f64 / b as f64), (Int(a), Int(b)) => Float(a as f64 / b as f64),
(Int(a), Float(b)) => Float(a as f64 / b), (Int(a), Float(b)) => Float(a as f64 / b),
(Float(a), Int(b)) => Float(a / b as f64), (Float(a), Int(b)) => Float(a / b as f64),
@ -182,31 +188,67 @@ pub fn div(lhs: Value, rhs: Value) -> Value {
(Linear(a), Int(b)) => Linear(a / b as f64), (Linear(a), Int(b)) => Linear(a / b as f64),
(Linear(a), Float(b)) => Linear(a / b), (Linear(a), Float(b)) => Linear(a / b),
_ => Error, (a, b) => mismatch!("cannot divide {} by {}", a, b),
} })
} }
/// Compute the logical "not" of a value. /// Compute the logical "not" of a value.
pub fn not(value: Value) -> Value { pub fn not(value: Value) -> StrResult<Value> {
match value { match value {
Bool(b) => Bool(!b), Bool(b) => Ok(Bool(!b)),
_ => Error, v => mismatch!("cannot apply 'not' to {}", v),
} }
} }
/// Compute the logical "and" of two values. /// Compute the logical "and" of two values.
pub fn and(lhs: Value, rhs: Value) -> Value { pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) { match (lhs, rhs) {
(Bool(a), Bool(b)) => Bool(a && b), (Bool(a), Bool(b)) => Ok(Bool(a && b)),
_ => Error, (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b),
} }
} }
/// Compute the logical "or" of two values. /// Compute the logical "or" of two values.
pub fn or(lhs: Value, rhs: Value) -> Value { pub fn or(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) { match (lhs, rhs) {
(Bool(a), Bool(b)) => Bool(a || b), (Bool(a), Bool(b)) => Ok(Bool(a || b)),
_ => Error, (a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b),
}
}
/// Compute whether two values are equal.
pub fn eq(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(Bool(equal(&lhs, &rhs)))
}
/// Compute whether two values are equal.
pub fn neq(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(Bool(!equal(&lhs, &rhs)))
}
macro_rules! comparison {
($name:ident, $op:tt, $($pat:tt)*) => {
/// Compute how a value compares with another value.
pub fn $name(lhs: Value, rhs: Value) -> StrResult<Value> {
if let Some(ordering) = compare(&lhs, &rhs) {
Ok(Bool(matches!(ordering, $($pat)*)))
} else {
mismatch!(concat!("cannot apply '", $op, "' to {} and {}"), lhs, rhs);
}
}
};
}
comparison!(lt, "<", Ordering::Less);
comparison!(leq, "<=", Ordering::Less | Ordering::Equal);
comparison!(gt, ">", Ordering::Greater);
comparison!(geq, ">=", Ordering::Greater | Ordering::Equal);
/// Compute the range from `lhs` to `rhs`.
pub fn range(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) {
(Int(a), Int(b)) => Ok(Array((a ..= b).map(Int).collect())),
(a, b) => mismatch!("cannot apply '..' to {} and {}", a, b),
} }
} }
@ -231,7 +273,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Template(a), Template(b)) => a == b, (Template(a), Template(b)) => a == b,
(Func(a), Func(b)) => a == b, (Func(a), Func(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b, (Dyn(a), Dyn(b)) => a == b,
(Error, Error) => true,
// Some technically different things should compare equal. // Some technically different things should compare equal.
(&Int(a), &Float(b)) => a as f64 == b, (&Int(a), &Float(b)) => a as f64 == b,
@ -245,16 +286,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
} }
} }
/// Compute whether two values are equal.
pub fn eq(lhs: Value, rhs: Value) -> Value {
Bool(equal(&lhs, &rhs))
}
/// Compute whether two values are equal.
pub fn neq(lhs: Value, rhs: Value) -> Value {
Bool(!equal(&lhs, &rhs))
}
/// Compare two values. /// Compare two values.
pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> { pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> {
match (lhs, rhs) { match (lhs, rhs) {
@ -269,26 +300,3 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> {
_ => Option::None, _ => Option::None,
} }
} }
macro_rules! comparison {
($name:ident, $($pat:tt)*) => {
/// Compute how a value compares with another value.
pub fn $name(lhs: Value, rhs: Value) -> Value {
compare(&lhs, &rhs)
.map_or(Error, |x| Bool(matches!(x, $($pat)*)))
}
};
}
comparison!(lt, Ordering::Less);
comparison!(leq, Ordering::Less | Ordering::Equal);
comparison!(gt, Ordering::Greater);
comparison!(geq, Ordering::Greater | Ordering::Equal);
/// Compute the range from `lhs` to `rhs`.
pub fn range(lhs: Value, rhs: Value) -> Value {
match (lhs, rhs) {
(Int(a), Int(b)) => Array((a ..= b).map(Int).collect()),
_ => Error,
}
}

View File

@ -4,7 +4,9 @@ use std::fmt::{self, Debug, Formatter};
use std::iter; use std::iter;
use std::rc::Rc; use std::rc::Rc;
use super::{EcoString, EvalContext, FuncArgs, Function, Value}; use super::{EvalContext, FuncArgs, Function, Value};
use crate::diag::TypResult;
use crate::util::EcoString;
/// A slot where a variable is stored. /// A slot where a variable is stored.
pub type Slot = Rc<RefCell<Value>>; pub type Slot = Rc<RefCell<Value>>;
@ -89,7 +91,7 @@ impl Scope {
/// Define a constant function. /// Define a constant function.
pub fn def_func<F>(&mut self, name: impl Into<EcoString>, f: F) pub fn def_func<F>(&mut self, name: impl Into<EcoString>, f: F)
where where
F: Fn(&mut EvalContext, &mut FuncArgs) -> Value + 'static, F: Fn(&mut EvalContext, &mut FuncArgs) -> TypResult<Value> + 'static,
{ {
let name = name.into(); let name = name.into();
self.def_const(name.clone(), Function::new(Some(name), f)); self.def_const(name.clone(), Function::new(Some(name), f));

View File

@ -21,9 +21,16 @@ impl Template {
} }
/// Iterate over the contained template nodes. /// Iterate over the contained template nodes.
pub fn iter(&self) -> impl Iterator<Item = &TemplateNode> + '_ { pub fn iter(&self) -> std::slice::Iter<TemplateNode> {
self.nodes.iter() self.nodes.iter()
} }
/// Repeat this template `n` times.
pub fn repeat(&self, n: usize) -> Self {
let len = self.nodes.len().checked_mul(n).expect("capacity overflow");
let nodes = self.iter().cloned().cycle().take(len).collect();
Self { nodes: Rc::new(nodes) }
}
} }
impl From<TemplateTree> for Template { impl From<TemplateTree> for Template {

View File

@ -5,10 +5,11 @@ use std::rc::Rc;
use super::{ops, Array, Dict, Function, Template, TemplateFunc}; use super::{ops, Array, Dict, Function, Template, TemplateFunc};
use crate::color::{Color, RgbaColor}; use crate::color::{Color, RgbaColor};
use crate::util::EcoString; use crate::diag::StrResult;
use crate::exec::ExecContext; use crate::exec::ExecContext;
use crate::geom::{Angle, Fractional, Length, Linear, Relative}; use crate::geom::{Angle, Fractional, Length, Linear, Relative};
use crate::syntax::Spanned; use crate::syntax::Spanned;
use crate::util::EcoString;
/// A computational value. /// A computational value.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -47,8 +48,6 @@ pub enum Value {
Func(Function), Func(Function),
/// A dynamic value. /// A dynamic value.
Dyn(Dynamic), Dyn(Dynamic),
/// The result of invalid operations.
Error,
} }
impl Value { impl Value {
@ -80,7 +79,6 @@ impl Value {
Self::Template(_) => Template::TYPE_NAME, Self::Template(_) => Template::TYPE_NAME,
Self::Func(_) => Function::TYPE_NAME, Self::Func(_) => Function::TYPE_NAME,
Self::Dyn(v) => v.type_name(), Self::Dyn(v) => v.type_name(),
Self::Error => "error",
} }
} }
@ -93,7 +91,7 @@ impl Value {
} }
/// Try to cast the value into a specific type. /// Try to cast the value into a specific type.
pub fn cast<T>(self) -> Result<T, String> pub fn cast<T>(self) -> StrResult<T>
where where
T: Cast<Value>, T: Cast<Value>,
{ {
@ -241,7 +239,7 @@ pub trait Cast<V>: Sized {
fn is(value: &V) -> bool; fn is(value: &V) -> bool;
/// Try to cast the value into an instance of `Self`. /// Try to cast the value into an instance of `Self`.
fn cast(value: V) -> Result<Self, String>; fn cast(value: V) -> StrResult<Self>;
} }
impl Cast<Value> for Value { impl Cast<Value> for Value {
@ -249,7 +247,7 @@ impl Cast<Value> for Value {
true true
} }
fn cast(value: Value) -> Result<Self, String> { fn cast(value: Value) -> StrResult<Self> {
Ok(value) Ok(value)
} }
} }
@ -262,7 +260,7 @@ where
T::is(&value.v) T::is(&value.v)
} }
fn cast(value: Spanned<Value>) -> Result<Self, String> { fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v) T::cast(value.v)
} }
} }
@ -275,7 +273,7 @@ where
T::is(&value.v) T::is(&value.v)
} }
fn cast(value: Spanned<Value>) -> Result<Self, String> { fn cast(value: Spanned<Value>) -> StrResult<Self> {
let span = value.span; let span = value.span;
T::cast(value.v).map(|t| Spanned::new(t, span)) T::cast(value.v).map(|t| Spanned::new(t, span))
} }
@ -302,7 +300,7 @@ macro_rules! primitive {
matches!(value, Value::$variant(_) $(| Value::$other(_))*) matches!(value, Value::$variant(_) $(| Value::$other(_))*)
} }
fn cast(value: Value) -> Result<Self, String> { fn cast(value: Value) -> StrResult<Self> {
match value { match value {
Value::$variant(v) => Ok(v), Value::$variant(v) => Ok(v),
$(Value::$other($binding) => Ok($out),)* $(Value::$other($binding) => Ok($out),)*
@ -358,7 +356,7 @@ macro_rules! castable {
} }
} }
fn cast(value: $crate::eval::Value) -> Result<Self, String> { fn cast(value: $crate::eval::Value) -> $crate::diag::StrResult<Self> {
let found = match value { let found = match value {
$($pattern => return Ok($out),)* $($pattern => return Ok($out),)*
$crate::eval::Value::Dyn(dynamic) => { $crate::eval::Value::Dyn(dynamic) => {
@ -387,6 +385,6 @@ primitive! { Color: "color", Color }
primitive! { EcoString: "string", Str } primitive! { EcoString: "string", Str }
primitive! { Array: "array", Array } primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict } primitive! { Dict: "dictionary", Dict }
primitive! { Template: "template", Template, Str(v) => v.into() } primitive! { Template: "template", Template }
primitive! { Function: "function", Func } primitive! { Function: "function", Func }
primitive! { f64: "float", Float, Int(v) => v as f64 } primitive! { f64: "float", Float, Int(v) => v as f64 }

View File

@ -2,13 +2,12 @@ use std::mem;
use std::rc::Rc; use std::rc::Rc;
use super::{Exec, ExecWithMap, State}; use super::{Exec, ExecWithMap, State};
use crate::diag::{Diag, DiagSet, Pass};
use crate::eval::{ExprMap, Template}; use crate::eval::{ExprMap, Template};
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
use crate::layout::{ use crate::layout::{
LayoutNode, LayoutTree, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, LayoutNode, LayoutTree, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode,
}; };
use crate::syntax::{Span, SyntaxTree}; use crate::syntax::SyntaxTree;
use crate::util::EcoString; use crate::util::EcoString;
use crate::Context; use crate::Context;
@ -16,8 +15,6 @@ use crate::Context;
pub struct ExecContext { pub struct ExecContext {
/// The active execution state. /// The active execution state.
pub state: State, pub state: State,
/// Execution diagnostics.
pub diags: DiagSet,
/// The tree of finished page runs. /// The tree of finished page runs.
tree: LayoutTree, tree: LayoutTree,
/// When we are building the top-level stack, this contains metrics of the /// When we are building the top-level stack, this contains metrics of the
@ -32,18 +29,12 @@ impl ExecContext {
pub fn new(ctx: &mut Context) -> Self { pub fn new(ctx: &mut Context) -> Self {
Self { Self {
state: ctx.state.clone(), state: ctx.state.clone(),
diags: DiagSet::new(),
tree: LayoutTree { runs: vec![] }, tree: LayoutTree { runs: vec![] },
page: Some(PageBuilder::new(&ctx.state, true)), page: Some(PageBuilder::new(&ctx.state, true)),
stack: StackBuilder::new(&ctx.state), stack: StackBuilder::new(&ctx.state),
} }
} }
/// Add a diagnostic.
pub fn diag(&mut self, diag: Diag) {
self.diags.insert(diag);
}
/// Execute a template and return the result as a stack node. /// Execute a template and return the result as a stack node.
pub fn exec_template_stack(&mut self, template: &Template) -> StackNode { pub fn exec_template_stack(&mut self, template: &Template) -> StackNode {
self.exec_stack(|ctx| template.exec(ctx)) self.exec_stack(|ctx| template.exec(ctx))
@ -127,21 +118,19 @@ impl ExecContext {
} }
/// Apply a forced page break. /// Apply a forced page break.
pub fn pagebreak(&mut self, keep: bool, hard: bool, span: Span) { pub fn pagebreak(&mut self, keep: bool, hard: bool) {
if let Some(builder) = &mut self.page { if let Some(builder) = &mut self.page {
let page = mem::replace(builder, PageBuilder::new(&self.state, hard)); let page = mem::replace(builder, PageBuilder::new(&self.state, hard));
let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state)); let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state));
self.tree.runs.extend(page.build(stack.build(), keep)); self.tree.runs.extend(page.build(stack.build(), keep));
} else {
self.diag(error!(span, "cannot modify page from here"));
} }
} }
/// Finish execution and return the created layout tree. /// Finish execution and return the created layout tree.
pub fn finish(mut self) -> Pass<LayoutTree> { pub fn finish(mut self) -> LayoutTree {
assert!(self.page.is_some()); assert!(self.page.is_some());
self.pagebreak(true, false, Span::default()); self.pagebreak(true, false);
Pass::new(self.tree, self.diags) self.tree
} }
fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild { fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild {

View File

@ -8,7 +8,6 @@ pub use state::*;
use std::fmt::Write; use std::fmt::Write;
use crate::diag::Pass;
use crate::eval::{ExprMap, Template, TemplateFunc, TemplateNode, TemplateTree, Value}; use crate::eval::{ExprMap, Template, TemplateFunc, TemplateNode, TemplateTree, Value};
use crate::geom::Gen; use crate::geom::Gen;
use crate::layout::{LayoutTree, StackChild, StackNode}; use crate::layout::{LayoutTree, StackChild, StackNode};
@ -18,7 +17,7 @@ use crate::util::EcoString;
use crate::Context; use crate::Context;
/// Execute a template to produce a layout tree. /// Execute a template to produce a layout tree.
pub fn exec(ctx: &mut Context, template: &Template) -> Pass<LayoutTree> { pub fn exec(ctx: &mut Context, template: &Template) -> LayoutTree {
let mut ctx = ExecContext::new(ctx); let mut ctx = ExecContext::new(ctx);
template.exec(&mut ctx); template.exec(&mut ctx);
ctx.finish() ctx.finish()
@ -138,7 +137,6 @@ impl Exec for Value {
Value::Float(v) => ctx.push_text(pretty(v)), Value::Float(v) => ctx.push_text(pretty(v)),
Value::Str(v) => ctx.push_text(v), Value::Str(v) => ctx.push_text(v),
Value::Template(v) => v.exec(ctx), Value::Template(v) => v.exec(ctx),
Value::Error => {}
// For values which can't be shown "naturally", we print the // For values which can't be shown "naturally", we print the
// representation in monospace. // representation in monospace.
other => ctx.push_monospace_text(pretty(other)), other => ctx.push_monospace_text(pretty(other)),

View File

@ -316,7 +316,8 @@ impl FontCache {
} }
/// A unique identifier for a loaded font face. /// A unique identifier for a loaded font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
pub struct FaceId(u64); pub struct FaceId(u64);
impl FaceId { impl FaceId {

View File

@ -103,7 +103,8 @@ impl ImageCache {
} }
/// A unique identifier for a loaded image. /// A unique identifier for a loaded image.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
pub struct ImageId(u64); pub struct ImageId(u64);
impl ImageId { impl ImageId {

View File

@ -7,9 +7,9 @@
//! module. //! module.
//! - **Evaluation:** The next step is to [evaluate] the syntax tree. This //! - **Evaluation:** The next step is to [evaluate] the syntax tree. This
//! computes the value of each node in the document and produces a [module]. //! computes the value of each node in the document and produces a [module].
//! - **Execution:** Now, we can [execute] the parsed and evaluated module. //! - **Execution:** Now, we can [execute] the parsed and evaluated module. This
//! This produces a [layout tree], a high-level, fully styled representation //! results in a [layout tree], a high-level, fully styled representation of
//! of the document. The nodes of this tree are self-contained and //! the document. The nodes of this tree are self-contained and
//! order-independent and thus much better suited for layouting than the //! order-independent and thus much better suited for layouting than the
//! syntax tree. //! syntax tree.
//! - **Layouting:** Next, the tree is [layouted] into a portable version of the //! - **Layouting:** Next, the tree is [layouted] into a portable version of the
@ -49,7 +49,7 @@ pub mod util;
use std::rc::Rc; use std::rc::Rc;
use crate::diag::Pass; use crate::diag::TypResult;
use crate::eval::{ModuleCache, Scope}; use crate::eval::{ModuleCache, Scope};
use crate::exec::State; use crate::exec::State;
use crate::font::FontCache; use crate::font::FontCache;
@ -100,19 +100,15 @@ impl Context {
/// The `file` identifies the source file and is used to resolve relative /// The `file` identifies the source file and is used to resolve relative
/// paths (for importing and image loading). /// paths (for importing and image loading).
/// ///
/// Returns a vector of frames representing individual pages alongside /// Returns either a vector of frames representing individual pages or
/// diagnostic information (errors and warnings). /// diagnostics in the form of a vector of error message with file and span
pub fn typeset(&mut self, file: FileId, src: &str) -> Pass<Vec<Rc<Frame>>> { /// information.
let ast = parse::parse(src); pub fn typeset(&mut self, file: FileId, src: &str) -> TypResult<Vec<Rc<Frame>>> {
let module = eval::eval(self, file, Rc::new(ast.output)); let ast = parse::parse(file, src)?;
let tree = exec::exec(self, &module.output.template); let module = eval::eval(self, file, Rc::new(ast))?;
let frames = layout::layout(self, &tree.output); let tree = exec::exec(self, &module.template);
let frames = layout::layout(self, &tree);
let mut diags = ast.diags; Ok(frames)
diags.extend(module.diags);
diags.extend(tree.diags);
Pass::new(frames, diags)
} }
} }

View File

@ -8,46 +8,44 @@ use crate::layout::{
}; };
/// `image`: An image. /// `image`: An image.
pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let path = args.expect::<Spanned<EcoString>>(ctx, "path to image file"); let path = args.expect::<Spanned<EcoString>>("path to image file")?;
let width = args.named(ctx, "width"); let width = args.named("width")?;
let height = args.named(ctx, "height"); let height = args.named("height")?;
let mut node = None; let file = ctx.resolve(&path.v, path.span)?;
if let Some(path) = &path { let node = match ctx.images.load(file) {
if let Some(file) = ctx.resolve(&path.v, path.span) { Some(id) => ImageNode { id, width, height },
if let Some(id) = ctx.images.load(file) { None => bail!(args.file, path.span, "failed to load image"),
node = Some(ImageNode { id, width, height }); };
} else {
ctx.diag(error!(path.span, "failed to load image"));
}
}
}
Value::template(move |ctx| { Ok(Value::template(move |ctx| ctx.push_into_par(node)))
if let Some(node) = node {
ctx.push_into_par(node);
}
})
} }
/// `rect`: A rectangle with optional content. /// `rect`: A rectangle with optional content.
pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn rect(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let width = args.named(ctx, "width"); let width = args.named("width")?;
let height = args.named(ctx, "height"); let height = args.named("height")?;
let fill = args.named(ctx, "fill"); let fill = args.named("fill")?;
let body = args.eat().unwrap_or_default(); let body = args.eat().unwrap_or_default();
rect_impl(width, height, None, fill, body) Ok(rect_impl(width, height, None, fill, body))
} }
/// `square`: A square with optional content. /// `square`: A square with optional content.
pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn square(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let length = args.named::<Length>(ctx, "length").map(Linear::from); let length = args.named::<Length>("length")?.map(Linear::from);
let width = length.or_else(|| args.named(ctx, "width")); let width = match length {
let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); Some(length) => Some(length),
let fill = args.named(ctx, "fill"); None => args.named("width")?,
};
let height = match width {
Some(_) => None,
None => args.named("height")?,
};
let aspect = Some(N64::from(1.0));
let fill = args.named("fill")?;
let body = args.eat().unwrap_or_default(); let body = args.eat().unwrap_or_default();
rect_impl(width, height, Some(N64::from(1.0)), fill, body) Ok(rect_impl(width, height, aspect, fill, body))
} }
fn rect_impl( fn rect_impl(
@ -76,22 +74,29 @@ fn rect_impl(
} }
/// `ellipse`: An ellipse with optional content. /// `ellipse`: An ellipse with optional content.
pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn ellipse(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let width = args.named(ctx, "width"); let width = args.named("width")?;
let height = args.named(ctx, "height"); let height = args.named("height")?;
let fill = args.named(ctx, "fill"); let fill = args.named("fill")?;
let body = args.eat().unwrap_or_default(); let body = args.eat().unwrap_or_default();
ellipse_impl(width, height, None, fill, body) Ok(ellipse_impl(width, height, None, fill, body))
} }
/// `circle`: A circle with optional content. /// `circle`: A circle with optional content.
pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn circle(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let diameter = args.named::<Length>(ctx, "radius").map(|r| 2.0 * Linear::from(r)); let diameter = args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r));
let width = diameter.or_else(|| args.named(ctx, "width")); let width = match diameter {
let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); None => args.named("width")?,
let fill = args.named(ctx, "fill"); diameter => diameter,
};
let height = match width {
None => args.named("height")?,
width => width,
};
let aspect = Some(N64::from(1.0));
let fill = args.named("fill")?;
let body = args.eat().unwrap_or_default(); let body = args.eat().unwrap_or_default();
ellipse_impl(width, height, Some(N64::from(1.0)), fill, body) Ok(ellipse_impl(width, height, aspect, fill, body))
} }
fn ellipse_impl( fn ellipse_impl(

View File

@ -3,26 +3,26 @@ use crate::layout::{FixedNode, GridNode, PadNode, StackChild, StackNode, TrackSi
use crate::paper::{Paper, PaperClass}; use crate::paper::{Paper, PaperClass};
/// `page`: Configure pages. /// `page`: Configure pages.
pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn page(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let span = args.span; let paper = match args.eat::<Spanned<EcoString>>() {
let paper = args.eat::<Spanned<EcoString>>().and_then(|name| { Some(name) => match Paper::from_name(&name.v) {
Paper::from_name(&name.v).or_else(|| { None => bail!(args.file, name.span, "invalid paper name"),
ctx.diag(error!(name.span, "invalid paper name")); paper => paper,
None },
}) None => None,
}); };
let width = args.named(ctx, "width"); let width = args.named("width")?;
let height = args.named(ctx, "height"); let height = args.named("height")?;
let margins = args.named(ctx, "margins"); let margins = args.named("margins")?;
let left = args.named(ctx, "left"); let left = args.named("left")?;
let top = args.named(ctx, "top"); let top = args.named("top")?;
let right = args.named(ctx, "right"); let right = args.named("right")?;
let bottom = args.named(ctx, "bottom"); let bottom = args.named("bottom")?;
let flip = args.named(ctx, "flip"); let flip = args.named("flip")?;
let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); let body = args.expect::<Template>("body")?;
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let snapshot = ctx.state.clone(); let snapshot = ctx.state.clone();
let state = ctx.state.page_mut(); let state = ctx.state.page_mut();
@ -65,50 +65,45 @@ pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
std::mem::swap(&mut state.size.width, &mut state.size.height); std::mem::swap(&mut state.size.width, &mut state.size.height);
} }
ctx.pagebreak(false, true, span); ctx.pagebreak(false, true);
body.exec(ctx); body.exec(ctx);
ctx.state = snapshot; ctx.state = snapshot;
ctx.pagebreak(true, false, span); ctx.pagebreak(true, false);
}) }))
} }
/// `pagebreak`: Start a new page. /// `pagebreak`: Start a new page.
pub fn pagebreak(_: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn pagebreak(_: &mut EvalContext, _: &mut FuncArgs) -> TypResult<Value> {
let span = args.span; Ok(Value::template(move |ctx| ctx.pagebreak(true, true)))
Value::template(move |ctx| {
ctx.pagebreak(true, true, span);
})
} }
/// `h`: Horizontal spacing. /// `h`: Horizontal spacing.
pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn h(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
spacing_impl(ctx, args, GenAxis::Cross) spacing_impl(args, GenAxis::Cross)
} }
/// `v`: Vertical spacing. /// `v`: Vertical spacing.
pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn v(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
spacing_impl(ctx, args, GenAxis::Main) spacing_impl(args, GenAxis::Main)
} }
fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: GenAxis) -> Value { fn spacing_impl(args: &mut FuncArgs, axis: GenAxis) -> TypResult<Value> {
let spacing: Option<Linear> = args.expect(ctx, "spacing"); let spacing = args.expect::<Linear>("spacing")?;
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
if let Some(linear) = spacing { // TODO: Should this really always be font-size relative?
// TODO: Should this really always be font-size relative? let amount = spacing.resolve(ctx.state.font.size);
let amount = linear.resolve(ctx.state.font.size); ctx.push_spacing(axis, amount);
ctx.push_spacing(axis, amount); }))
}
})
} }
/// `align`: Configure the alignment along the layouting axes. /// `align`: Configure the alignment along the layouting axes.
pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn align(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let mut horizontal = args.named("horizontal")?;
let mut vertical = args.named("vertical")?;
let first = args.eat::<Align>(); let first = args.eat::<Align>();
let second = args.eat::<Align>(); let second = args.eat::<Align>();
let mut horizontal = args.named(ctx, "horizontal"); let body = args.expect::<Template>("body")?;
let mut vertical = args.named(ctx, "vertical");
let body = args.expect::<Template>(ctx, "body").unwrap_or_default();
for value in first.into_iter().chain(second) { for value in first.into_iter().chain(second) {
match value.axis() { match value.axis() {
@ -122,7 +117,7 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
} }
} }
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
if let Some(horizontal) = horizontal { if let Some(horizontal) = horizontal {
ctx.state.aligns.cross = horizontal; ctx.state.aligns.cross = horizontal;
} }
@ -133,37 +128,37 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
} }
body.exec(ctx); body.exec(ctx);
}) }))
} }
/// `box`: Place content in a rectangular box. /// `box`: Place content in a rectangular box.
pub fn boxed(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn boxed(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let width = args.named(ctx, "width"); let width = args.named("width")?;
let height = args.named(ctx, "height"); let height = args.named("height")?;
let body = args.eat().unwrap_or_default(); let body = args.eat().unwrap_or_default();
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let child = ctx.exec_template_stack(&body).into(); let child = ctx.exec_template_stack(&body).into();
ctx.push_into_par(FixedNode { width, height, child }); ctx.push_into_par(FixedNode { width, height, child });
}) }))
} }
/// `block`: Place content in a block. /// `block`: Place content in a block.
pub fn block(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn block(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let body = args.expect(ctx, "body").unwrap_or_default(); let body = args.expect("body")?;
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let block = ctx.exec_template_stack(&body); let block = ctx.exec_template_stack(&body);
ctx.push_into_stack(block); ctx.push_into_stack(block);
}) }))
} }
/// `pad`: Pad content at the sides. /// `pad`: Pad content at the sides.
pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn pad(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let all = args.eat(); let all = args.eat();
let left = args.named(ctx, "left"); let left = args.named("left")?;
let top = args.named(ctx, "top"); let top = args.named("top")?;
let right = args.named(ctx, "right"); let right = args.named("right")?;
let bottom = args.named(ctx, "bottom"); let bottom = args.named("bottom")?;
let body = args.expect(ctx, "body").unwrap_or_default(); let body = args.expect("body")?;
let padding = Sides::new( let padding = Sides::new(
left.or(all).unwrap_or_default(), left.or(all).unwrap_or_default(),
@ -172,18 +167,18 @@ pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
bottom.or(all).unwrap_or_default(), bottom.or(all).unwrap_or_default(),
); );
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let child = ctx.exec_template_stack(&body).into(); let child = ctx.exec_template_stack(&body).into();
ctx.push_into_stack(PadNode { padding, child }); ctx.push_into_stack(PadNode { padding, child });
}) }))
} }
/// `stack`: Stack children along an axis. /// `stack`: Stack children along an axis.
pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn stack(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let dir = args.named(ctx, "dir"); let dir = args.named("dir")?;
let children: Vec<_> = args.all().collect(); let children: Vec<_> = args.all().collect();
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let children = children let children = children
.iter() .iter()
.map(|child| { .map(|child| {
@ -201,23 +196,23 @@ pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
} }
ctx.push_into_stack(StackNode { dirs, aspect: None, children }); ctx.push_into_stack(StackNode { dirs, aspect: None, children });
}) }))
} }
/// `grid`: Arrange children into a grid. /// `grid`: Arrange children into a grid.
pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn grid(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let columns = args.named(ctx, "columns").unwrap_or_default(); let columns = args.named("columns")?.unwrap_or_default();
let rows = args.named(ctx, "rows").unwrap_or_default(); let rows = args.named("rows")?.unwrap_or_default();
let gutter_columns = args.named(ctx, "gutter-columns"); let gutter_columns = args.named("gutter-columns")?;
let gutter_rows = args.named(ctx, "gutter-rows"); let gutter_rows = args.named("gutter-rows")?;
let default = args let default = args
.named(ctx, "gutter") .named("gutter")?
.map(|v| vec![TrackSizing::Linear(v)]) .map(|v| vec![TrackSizing::Linear(v)])
.unwrap_or_default(); .unwrap_or_default();
let column_dir = args.named(ctx, "column-dir"); let column_dir = args.named("column-dir")?;
let row_dir = args.named(ctx, "row-dir"); let row_dir = args.named("row-dir")?;
let children: Vec<_> = args.all().collect(); let children: Vec<_> = args.all().collect();
@ -227,7 +222,7 @@ pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
gutter_rows.unwrap_or(default), gutter_rows.unwrap_or(default),
); );
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let children = children let children = children
.iter() .iter()
.map(|child| ctx.exec_template_stack(child).into()) .map(|child| ctx.exec_template_stack(child).into())
@ -257,7 +252,7 @@ pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
gutter: gutter.clone(), gutter: gutter.clone(),
children, children,
}) })
}) }))
} }
/// Defines size of rows and columns in a grid. /// Defines size of rows and columns in a grid.

View File

@ -17,6 +17,7 @@ use std::convert::TryFrom;
use std::rc::Rc; use std::rc::Rc;
use crate::color::{Color, RgbaColor}; use crate::color::{Color, RgbaColor};
use crate::diag::TypResult;
use crate::eval::{EvalContext, FuncArgs, Scope, Template, Type, Value}; use crate::eval::{EvalContext, FuncArgs, Scope, Template, Type, Value};
use crate::exec::Exec; use crate::exec::Exec;
use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric};

View File

@ -4,29 +4,27 @@ use crate::layout::Paint;
use super::*; use super::*;
/// `font`: Configure the font. /// `font`: Configure the font.
pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn font(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let size = args.eat::<Linear>().or_else(|| args.named(ctx, "size")); let size = args.named::<Linear>("size")?.or_else(|| args.eat());
let style = args.named(ctx, "style"); let style = args.named("style")?;
let weight = args.named(ctx, "weight"); let weight = args.named("weight")?;
let stretch = args.named(ctx, "stretch"); let stretch = args.named("stretch")?;
let top_edge = args.named(ctx, "top-edge"); let top_edge = args.named("top-edge")?;
let bottom_edge = args.named(ctx, "bottom-edge"); let bottom_edge = args.named("bottom-edge")?;
let fill = args.named(ctx, "fill"); let fill = args.named("fill")?;
let families: Vec<_> = args.all().collect(); let list = args.named("family")?.or_else(|| {
let list = if families.is_empty() { let families: Vec<_> = args.all().collect();
args.named(ctx, "family") (!families.is_empty()).then(|| FontDef(Rc::new(families)))
} else { });
Some(FontDef(Rc::new(families)))
};
let serif = args.named(ctx, "serif"); let serif = args.named("serif")?;
let sans_serif = args.named(ctx, "sans-serif"); let sans_serif = args.named("sans-serif")?;
let monospace = args.named(ctx, "monospace"); let monospace = args.named("monospace")?;
let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); let body = args.expect::<Template>("body")?;
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let state = ctx.state.font_mut(); let state = ctx.state.font_mut();
if let Some(size) = size { if let Some(size) = size {
@ -74,7 +72,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
} }
body.exec(ctx); body.exec(ctx);
}) }))
} }
struct FontDef(Rc<Vec<FontFamily>>); struct FontDef(Rc<Vec<FontFamily>>);
@ -106,12 +104,12 @@ castable! {
} }
/// `par`: Configure paragraphs. /// `par`: Configure paragraphs.
pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn par(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let par_spacing = args.named(ctx, "spacing"); let par_spacing = args.named("spacing")?;
let line_spacing = args.named(ctx, "leading"); let line_spacing = args.named("leading")?;
let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); let body = args.expect::<Template>("body")?;
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
let state = ctx.state.par_mut(); let state = ctx.state.par_mut();
if let Some(par_spacing) = par_spacing { if let Some(par_spacing) = par_spacing {
@ -124,33 +122,32 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
ctx.parbreak(); ctx.parbreak();
body.exec(ctx); body.exec(ctx);
}) }))
} }
/// `lang`: Configure the language. /// `lang`: Configure the language.
pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn lang(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let iso = args.eat::<EcoString>(); let iso = args.eat::<EcoString>();
let dir = if let Some(dir) = args.named::<Spanned<Dir>>(ctx, "dir") { let dir = if let Some(dir) = args.named::<Spanned<Dir>>("dir")? {
if dir.v.axis() == SpecAxis::Horizontal { if dir.v.axis() == SpecAxis::Horizontal {
Some(dir.v) Some(dir.v)
} else { } else {
ctx.diag(error!(dir.span, "must be horizontal")); bail!(args.file, dir.span, "must be horizontal");
None
} }
} else { } else {
iso.as_deref().map(lang_dir) iso.as_deref().map(lang_dir)
}; };
let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); let body = args.expect::<Template>("body")?;
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
if let Some(dir) = dir { if let Some(dir) = dir {
ctx.state.dirs.cross = dir; ctx.state.dirs.cross = dir;
} }
ctx.parbreak(); ctx.parbreak();
body.exec(ctx); body.exec(ctx);
}) }))
} }
/// The default direction for the language identified by the given `iso` code. /// The default direction for the language identified by the given `iso` code.
@ -163,30 +160,29 @@ fn lang_dir(iso: &str) -> Dir {
} }
/// `strike`: Enable striken-through text. /// `strike`: Enable striken-through text.
pub fn strike(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn strike(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
line_impl(ctx, args, |font| &mut font.strikethrough) line_impl(args, |font| &mut font.strikethrough)
} }
/// `underline`: Enable underlined text. /// `underline`: Enable underlined text.
pub fn underline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn underline(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
line_impl(ctx, args, |font| &mut font.underline) line_impl(args, |font| &mut font.underline)
} }
/// `overline`: Add an overline above text. /// `overline`: Add an overline above text.
pub fn overline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn overline(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
line_impl(ctx, args, |font| &mut font.overline) line_impl(args, |font| &mut font.overline)
} }
fn line_impl( fn line_impl(
ctx: &mut EvalContext,
args: &mut FuncArgs, args: &mut FuncArgs,
substate: fn(&mut FontState) -> &mut Option<Rc<LineState>>, substate: fn(&mut FontState) -> &mut Option<Rc<LineState>>,
) -> Value { ) -> TypResult<Value> {
let stroke = args.eat().or_else(|| args.named(ctx, "stroke")); let stroke = args.named("stroke")?.or_else(|| args.eat());
let thickness = args.eat::<Linear>().or_else(|| args.named(ctx, "thickness")); let thickness = args.named::<Linear>("thickness")?.or_else(|| args.eat());
let offset = args.named(ctx, "offset"); let offset = args.named("offset")?;
let extent = args.named(ctx, "extent").unwrap_or_default(); let extent = args.named("extent")?.unwrap_or_default();
let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); let body = args.expect::<Template>("body")?;
// Suppress any existing strikethrough if strength is explicitly zero. // Suppress any existing strikethrough if strength is explicitly zero.
let state = thickness.map_or(true, |s| !s.is_zero()).then(|| { let state = thickness.map_or(true, |s| !s.is_zero()).then(|| {
@ -198,8 +194,8 @@ fn line_impl(
}) })
}); });
Value::template(move |ctx| { Ok(Value::template(move |ctx| {
*substate(ctx.state.font_mut()) = state.clone(); *substate(ctx.state.font_mut()) = state.clone();
body.exec(ctx); body.exec(ctx);
}) }))
} }

View File

@ -7,94 +7,78 @@ use crate::pretty::pretty;
use super::*; use super::*;
/// `type`: The name of a value's type. /// `type`: The name of a value's type.
pub fn type_(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn type_(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
match args.expect::<Value>(ctx, "value") { let value = args.expect::<Value>("value")?;
Some(value) => value.type_name().into(), Ok(value.type_name().into())
None => Value::Error,
}
} }
/// `repr`: The string representation of a value. /// `repr`: The string representation of a value.
pub fn repr(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn repr(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
match args.expect::<Value>(ctx, "value") { let value = args.expect::<Value>("value")?;
Some(value) => pretty(&value).into(), Ok(pretty(&value).into())
None => Value::Error,
}
} }
/// `len`: The length of a string, an array or a dictionary. /// `len`: The length of a string, an array or a dictionary.
pub fn len(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn len(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
match args.expect(ctx, "collection") { let Spanned { v, span } = args.expect("collection")?;
Some(Spanned { v: Value::Str(v), .. }) => Value::Int(v.len() as i64), Ok(match v {
Some(Spanned { v: Value::Array(v), .. }) => Value::Int(v.len() as i64), Value::Str(v) => Value::Int(v.len() as i64),
Some(Spanned { v: Value::Dict(v), .. }) => Value::Int(v.len() as i64), Value::Array(v) => Value::Int(v.len() as i64),
Some(other) if other.v != Value::Error => { Value::Dict(v) => Value::Int(v.len() as i64),
ctx.diag(error!(other.span, "expected string, array or dictionary")); _ => bail!(args.file, span, "expected string, array or dictionary"),
Value::Error })
}
_ => Value::Error,
}
} }
/// `rgb`: Create an RGB(A) color. /// `rgb`: Create an RGB(A) color.
pub fn rgb(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn rgb(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
Value::Color(Color::Rgba( Ok(Value::Color(Color::Rgba(
if let Some(string) = args.eat::<Spanned<EcoString>>() { if let Some(string) = args.eat::<Spanned<EcoString>>() {
match RgbaColor::from_str(&string.v) { match RgbaColor::from_str(&string.v) {
Ok(color) => color, Ok(color) => color,
Err(_) => { Err(_) => bail!(args.file, string.span, "invalid color"),
ctx.diag(error!(string.span, "invalid color"));
return Value::Error;
}
} }
} else { } else {
let r = args.expect(ctx, "red component").unwrap_or(0.0); let r = args.expect("red component")?;
let g = args.expect(ctx, "green component").unwrap_or(0.0); let g = args.expect("green component")?;
let b = args.expect(ctx, "blue component").unwrap_or(0.0); let b = args.expect("blue component")?;
let a = args.eat().unwrap_or(1.0); let a = args.eat().unwrap_or(1.0);
let f = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8; let f = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
RgbaColor::new(f(r), f(g), f(b), f(a)) RgbaColor::new(f(r), f(g), f(b), f(a))
}, },
)) )))
} }
/// `min`: The minimum of a sequence of values. /// `min`: The minimum of a sequence of values.
pub fn min(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn min(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
minmax(ctx, args, Ordering::Less) minmax(args, Ordering::Less)
} }
/// `max`: The maximum of a sequence of values. /// `max`: The maximum of a sequence of values.
pub fn max(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { pub fn max(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
minmax(ctx, args, Ordering::Greater) minmax(args, Ordering::Greater)
} }
/// Find the minimum or maximum of a sequence of values. /// Find the minimum or maximum of a sequence of values.
fn minmax(ctx: &mut EvalContext, args: &mut FuncArgs, goal: Ordering) -> Value { fn minmax(args: &mut FuncArgs, goal: Ordering) -> TypResult<Value> {
let span = args.span; let &mut FuncArgs { file, span, .. } = args;
let mut extremum = None;
let mut extremum = args.expect::<Value>("value")?;
for value in args.all::<Value>() { for value in args.all::<Value>() {
if let Some(prev) = &extremum { match value.partial_cmp(&extremum) {
match value.partial_cmp(&prev) { Some(ordering) => {
Some(ordering) if ordering == goal => extremum = Some(value), if ordering == goal {
Some(_) => {} extremum = value;
None => {
ctx.diag(error!(
span,
"cannot compare {} with {}",
prev.type_name(),
value.type_name(),
));
return Value::Error;
} }
} }
} else { None => bail!(
extremum = Some(value); file,
span,
"cannot compare {} with {}",
extremum.type_name(),
value.type_name(),
),
} }
} }
extremum.unwrap_or_else(|| { Ok(extremum)
args.expect::<Value>(ctx, "value");
Value::Error
})
} }

View File

@ -34,7 +34,8 @@ pub trait Loader {
/// A file id that can be [resolved](Loader::resolve_from) from a path. /// A file id that can be [resolved](Loader::resolve_from) from a path.
/// ///
/// Should be the same for all paths pointing to the same file. /// Should be the same for all paths pointing to the same file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
pub struct FileId(u64); pub struct FileId(u64);
impl FileId { impl FileId {

View File

@ -35,32 +35,35 @@ fn main() -> anyhow::Result<()> {
.wrap(); .wrap();
// Resolve the file id of the source file and read the file. // Resolve the file id of the source file and read the file.
let src_id = loader.resolve(src_path).context("source file not found")?; let file = loader.resolve(src_path).context("source file not found")?;
let src = fs::read_to_string(&src_path) let src = fs::read_to_string(&src_path)
.map_err(|_| anyhow!("failed to read source file"))?; .map_err(|_| anyhow!("failed to read source file"))?;
// Typeset. // Typeset.
let mut ctx = typst::Context::new(loader); let mut ctx = typst::Context::new(loader);
let pass = ctx.typeset(src_id, &src); match ctx.typeset(file, &src) {
// Export the PDF.
Ok(document) => {
let buffer = typst::export::pdf(&ctx, &document);
fs::write(&dest_path, buffer).context("failed to write PDF file")?;
}
// Print diagnostics. // Print diagnostics.
let map = typst::parse::LineMap::new(&src); Err(errors) => {
for diag in pass.diags { let map = typst::parse::LineMap::new(&src);
let start = map.location(diag.span.start).unwrap(); for error in errors.iter() {
let end = map.location(diag.span.end).unwrap(); let start = map.location(error.span.start).unwrap();
println!( let end = map.location(error.span.end).unwrap();
"{}: {}:{}-{}: {}", println!(
diag.level, "Error: {}:{}-{}: {}",
src_path.display(), src_path.display(),
start, start,
end, end,
diag.message, error.message,
); );
}
}
} }
// Export the PDF.
let buffer = typst::export::pdf(&ctx, &pass.output);
fs::write(&dest_path, buffer).context("failed to write PDF file")?;
Ok(()) Ok(())
} }

View File

@ -77,8 +77,8 @@ impl<'s> LineMap<'s> {
} }
} }
/// Determine the column at the end of the string. /// Count how many column the string would fill.
pub fn search_column(src: &str) -> usize { pub fn count_columns(src: &str) -> usize {
let mut column = 0; let mut column = 0;
for c in src.chars().rev() { for c in src.chars().rev() {
if is_newline(c) { if is_newline(c) {

View File

@ -14,14 +14,21 @@ pub use tokens::*;
use std::rc::Rc; use std::rc::Rc;
use crate::diag::Pass; use crate::diag::TypResult;
use crate::loading::FileId;
use crate::syntax::*; use crate::syntax::*;
use crate::util::EcoString; use crate::util::EcoString;
/// Parse a string of source code. /// Parse a string of source code.
pub fn parse(src: &str) -> Pass<SyntaxTree> { pub fn parse(file: FileId, src: &str) -> TypResult<SyntaxTree> {
let mut p = Parser::new(src); let mut p = Parser::new(file, src);
Pass::new(tree(&mut p), p.diags) let tree = tree(&mut p);
let errors = p.finish();
if errors.is_empty() {
Ok(tree)
} else {
Err(Box::new(errors))
}
} }
/// Parse a syntax tree. /// Parse a syntax tree.
@ -126,7 +133,7 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option<SyntaxNode> {
p.start_group(group, TokenMode::Code); p.start_group(group, TokenMode::Code);
let expr = expr_with(p, true, 0); let expr = expr_with(p, true, 0);
if stmt && expr.is_some() && !p.eof() { if stmt && expr.is_some() && !p.eof() {
p.expected_at("semicolon or line break", p.prev_end()); p.expected_at(p.prev_end(), "semicolon or line break");
} }
p.end_group(); p.end_group();
@ -160,12 +167,12 @@ fn unicode_escape(p: &mut Parser, token: UnicodeEscapeToken) -> EcoString {
c.into() c.into()
} else { } else {
// Print out the escape sequence verbatim if it is invalid. // Print out the escape sequence verbatim if it is invalid.
p.diag(error!(span, "invalid unicode escape sequence")); p.error(span, "invalid unicode escape sequence");
p.peek_src().into() p.peek_src().into()
}; };
if !token.terminated { if !token.terminated {
p.diag(error!(span.end, "expected closing brace")); p.error(span.end, "expected closing brace");
} }
text text
@ -176,7 +183,7 @@ fn raw(p: &mut Parser, token: RawToken) -> SyntaxNode {
let span = p.peek_span(); let span = p.peek_span();
let raw = resolve::resolve_raw(span, token.text, token.backticks); let raw = resolve::resolve_raw(span, token.text, token.backticks);
if !token.terminated { if !token.terminated {
p.diag(error!(p.peek_span().end, "expected backtick(s)")); p.error(span.end, "expected backtick(s)");
} }
SyntaxNode::Raw(raw) SyntaxNode::Raw(raw)
} }
@ -198,11 +205,7 @@ fn heading(p: &mut Parser) -> SyntaxNode {
let body = tree_indented(p); let body = tree_indented(p);
SyntaxNode::Heading(HeadingNode { SyntaxNode::Heading(HeadingNode { span: p.span(start), level, body })
span: p.span(start),
level,
body: Rc::new(body),
})
} }
/// Parse a single list item. /// Parse a single list item.
@ -356,7 +359,7 @@ fn literal(p: &mut Parser) -> Option<Expr> {
Token::Fraction(p) => Expr::Fractional(span, p), Token::Fraction(p) => Expr::Fractional(span, p),
Token::Str(token) => Expr::Str(span, { Token::Str(token) => Expr::Str(span, {
if !token.terminated { if !token.terminated {
p.expected_at("quote", p.peek_span().end); p.expected_at(span.end, "quote");
} }
resolve::resolve_string(token.string) resolve::resolve_string(token.string)
}), }),
@ -421,7 +424,7 @@ fn collection(p: &mut Parser) -> (Vec<CallArg>, bool) {
items.push(arg); items.push(arg);
if let Some(pos) = missing_coma.take() { if let Some(pos) = missing_coma.take() {
p.expected_at("comma", pos); p.expected_at(pos, "comma");
} }
if p.eof() { if p.eof() {
@ -447,7 +450,7 @@ fn item(p: &mut Parser) -> Option<CallArg> {
if let Expr::Ident(name) = first { if let Expr::Ident(name) = first {
Some(CallArg::Named(Named { name, expr: expr(p)? })) Some(CallArg::Named(Named { name, expr: expr(p)? }))
} else { } else {
p.diag(error!(first.span(), "expected identifier")); p.error(first.span(), "expected identifier");
expr(p); expr(p);
None None
} }
@ -461,7 +464,7 @@ fn array(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
let items = items.into_iter().filter_map(|item| match item { let items = items.into_iter().filter_map(|item| match item {
CallArg::Pos(expr) => Some(expr), CallArg::Pos(expr) => Some(expr),
CallArg::Named(_) => { CallArg::Named(_) => {
p.diag(error!(item.span(), "expected expression, found named pair")); p.error(item.span(), "expected expression, found named pair");
None None
} }
}); });
@ -474,7 +477,7 @@ fn dict(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
let items = items.into_iter().filter_map(|item| match item { let items = items.into_iter().filter_map(|item| match item {
CallArg::Named(named) => Some(named), CallArg::Named(named) => Some(named),
CallArg::Pos(_) => { CallArg::Pos(_) => {
p.diag(error!(item.span(), "expected named pair, found expression")); p.error(item.span(), "expected named pair, found expression");
None None
} }
}); });
@ -488,7 +491,7 @@ fn idents(p: &mut Parser, items: Vec<CallArg>) -> Vec<Ident> {
let items = items.into_iter().filter_map(|item| match item { let items = items.into_iter().filter_map(|item| match item {
CallArg::Pos(Expr::Ident(id)) => Some(id), CallArg::Pos(Expr::Ident(id)) => Some(id),
_ => { _ => {
p.diag(error!(item.span(), "expected identifier")); p.error(item.span(), "expected identifier");
None None
} }
}); });
@ -512,7 +515,7 @@ fn block(p: &mut Parser, scoping: bool) -> Expr {
if let Some(expr) = expr(p) { if let Some(expr) = expr(p) {
exprs.push(expr); exprs.push(expr);
if !p.eof() { if !p.eof() {
p.expected_at("semicolon or line break", p.prev_end()); p.expected_at(p.prev_end(), "semicolon or line break");
} }
} }
p.end_group(); p.end_group();
@ -529,10 +532,7 @@ fn call(p: &mut Parser, callee: Expr) -> Option<Expr> {
let mut wide = p.eat_if(Token::Excl); let mut wide = p.eat_if(Token::Excl);
if wide && p.outer_mode() == TokenMode::Code { if wide && p.outer_mode() == TokenMode::Code {
let span = p.span(callee.span().start); let span = p.span(callee.span().start);
p.diag(error!( p.error(span, "wide calls are only allowed directly in templates");
span,
"wide calls are only allowed directly in templates",
));
wide = false; wide = false;
} }
@ -548,7 +548,7 @@ fn call(p: &mut Parser, callee: Expr) -> Option<Expr> {
items: vec![], items: vec![],
}, },
_ => { _ => {
p.expected_at("argument list", p.prev_end()); p.expected_at(p.prev_end(), "argument list");
return None; return None;
} }
}; };
@ -616,7 +616,7 @@ fn let_expr(p: &mut Parser) -> Option<Expr> {
init = expr(p); init = expr(p);
} else if params.is_some() { } else if params.is_some() {
// Function definitions must have a body. // Function definitions must have a body.
p.expected_at("body", p.prev_end()); p.expected_at(p.prev_end(), "body");
} }
// Rewrite into a closure expression if it's a function definition. // Rewrite into a closure expression if it's a function definition.
@ -738,7 +738,7 @@ fn import_expr(p: &mut Parser) -> Option<Expr> {
p.start_group(Group::Imports, TokenMode::Code); p.start_group(Group::Imports, TokenMode::Code);
let items = collection(p).0; let items = collection(p).0;
if items.is_empty() { if items.is_empty() {
p.expected_at("import items", p.prev_end()); p.expected_at(p.prev_end(), "import items");
} }
p.end_group(); p.end_group();
Imports::Idents(idents(p, items)) Imports::Idents(idents(p, items))
@ -790,7 +790,7 @@ fn body(p: &mut Parser) -> Option<Expr> {
Some(Token::LeftBracket) => Some(template(p)), Some(Token::LeftBracket) => Some(template(p)),
Some(Token::LeftBrace) => Some(block(p, true)), Some(Token::LeftBrace) => Some(block(p, true)),
_ => { _ => {
p.expected_at("body", p.prev_end()); p.expected_at(p.prev_end(), "body");
None None
} }
} }

View File

@ -1,14 +1,17 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::Range; use std::ops::Range;
use super::{search_column, TokenMode, Tokens}; use super::{count_columns, TokenMode, Tokens};
use crate::diag::{Diag, DiagSet}; use crate::diag::Error;
use crate::loading::FileId;
use crate::syntax::{Pos, Span, Token}; use crate::syntax::{Pos, Span, Token};
/// A convenient token-based parser. /// A convenient token-based parser.
pub struct Parser<'s> { pub struct Parser<'s> {
/// Parsing diagnostics. /// The id of the parsed file.
pub diags: DiagSet, file: FileId,
/// Parsing errors.
errors: Vec<Error>,
/// An iterator over the source tokens. /// An iterator over the source tokens.
tokens: Tokens<'s>, tokens: Tokens<'s>,
/// The stack of open groups. /// The stack of open groups.
@ -57,11 +60,12 @@ pub enum Group {
impl<'s> Parser<'s> { impl<'s> Parser<'s> {
/// Create a new parser for the source string. /// Create a new parser for the source string.
pub fn new(src: &'s str) -> Self { pub fn new(file: FileId, src: &'s str) -> Self {
let mut tokens = Tokens::new(src, TokenMode::Markup); let mut tokens = Tokens::new(src, TokenMode::Markup);
let next = tokens.next(); let next = tokens.next();
Self { Self {
diags: DiagSet::new(), file,
errors: vec![],
tokens, tokens,
groups: vec![], groups: vec![],
next, next,
@ -71,39 +75,45 @@ impl<'s> Parser<'s> {
} }
} }
/// Add a diagnostic. /// Finish parsing and return all errors.
pub fn diag(&mut self, diag: Diag) { pub fn finish(self) -> Vec<Error> {
self.diags.insert(diag); self.errors
} }
/// Eat the next token and add a diagnostic that it is not the expected /// Add an error with location and message.
/// `thing`. pub fn error(&mut self, span: impl Into<Span>, message: impl Into<String>) {
self.errors.push(Error {
file: self.file,
span: span.into(),
message: message.into(),
});
}
/// Eat the next token and add an error that it is not the expected `thing`.
pub fn expected(&mut self, what: &str) { pub fn expected(&mut self, what: &str) {
let before = self.next_start(); let before = self.next_start();
if let Some(found) = self.eat() { if let Some(found) = self.eat() {
let after = self.prev_end(); let after = self.prev_end();
self.diag(error!( self.error(
before .. after, before .. after,
"expected {}, found {}", format!("expected {}, found {}", what, found.name()),
what, );
found.name(),
));
} else { } else {
self.expected_at(what, self.next_start()); self.expected_at(self.next_start(), what);
} }
} }
/// Add a diagnostic that `what` was expected at the given position. /// Add an error that `what` was expected at the given position.
pub fn expected_at(&mut self, what: &str, pos: impl Into<Pos>) { pub fn expected_at(&mut self, pos: impl Into<Pos>, what: &str) {
self.diag(error!(pos.into(), "expected {}", what)); self.error(pos.into(), format!("expected {}", what));
} }
/// Eat the next token and add a diagnostic that it is unexpected. /// Eat the next token and add an error that it is unexpected.
pub fn unexpected(&mut self) { pub fn unexpected(&mut self) {
let before = self.next_start(); let before = self.next_start();
if let Some(found) = self.eat() { if let Some(found) = self.eat() {
let after = self.prev_end(); let after = self.prev_end();
self.diag(error!(before .. after, "unexpected {}", found.name())); self.error(before .. after, format!("unexpected {}", found.name()));
} }
} }
@ -159,7 +169,7 @@ impl<'s> Parser<'s> {
self.bump(); self.bump();
rescan = false; rescan = false;
} else if required { } else if required {
self.diag(error!(self.next_start(), "expected {}", end.name())); self.error(self.next_start(), format!("expected {}", end.name()));
} }
} }
@ -276,12 +286,12 @@ impl<'s> Parser<'s> {
Span::new(start, self.prev_end()) Span::new(start, self.prev_end())
} }
/// Consume the next token if it is the given one and produce a diagnostic /// Consume the next token if it is the given one and produce an error if
/// if not. /// not.
pub fn expect(&mut self, t: Token) -> bool { pub fn expect(&mut self, t: Token) -> bool {
let eaten = self.eat_if(t); let eaten = self.eat_if(t);
if !eaten { if !eaten {
self.expected_at(t.name(), self.prev_end()); self.expected_at(self.prev_end(), t.name());
} }
eaten eaten
} }
@ -314,7 +324,7 @@ impl<'s> Parser<'s> {
/// Determine the column for the given index in the source. /// Determine the column for the given index in the source.
pub fn column(&self, index: usize) -> usize { pub fn column(&self, index: usize) -> usize {
search_column(self.tokens.scanner().get(.. index)) count_columns(self.tokens.scanner().get(.. index))
} }
/// The span from `start` to [`self.prev_end()`](Self::prev_end). /// The span from `start` to [`self.prev_end()`](Self::prev_end).

View File

@ -488,7 +488,6 @@ impl Pretty for Value {
Self::Template(v) => v.pretty(p), Self::Template(v) => v.pretty(p),
Self::Func(v) => v.pretty(p), Self::Func(v) => v.pretty(p),
Self::Dyn(v) => v.pretty(p), Self::Dyn(v) => v.pretty(p),
Self::Error => p.push_str("<error>"),
} }
} }
} }
@ -609,6 +608,7 @@ pretty_display! {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::loading::FileId;
use crate::parse::parse; use crate::parse::parse;
#[track_caller] #[track_caller]
@ -618,7 +618,7 @@ mod tests {
#[track_caller] #[track_caller]
fn test_parse(src: &str, exp: &str) { fn test_parse(src: &str, exp: &str) {
let ast = parse(src).output; let ast = parse(FileId::from_raw(0), src).unwrap();
let found = pretty(&ast); let found = pretty(&ast);
if exp != found { if exp != found {
println!("tree: {:#?}", ast); println!("tree: {:#?}", ast);
@ -732,6 +732,7 @@ mod tests {
#[test] #[test]
fn test_pretty_print_value() { fn test_pretty_print_value() {
// Primitives.
test_value(Value::None, "none"); test_value(Value::None, "none");
test_value(false, "false"); test_value(false, "false");
test_value(12i64, "12"); test_value(12i64, "12");
@ -742,6 +743,8 @@ mod tests {
test_value(Relative::new(0.3) + Length::cm(2.0), "30% + 2cm"); test_value(Relative::new(0.3) + Length::cm(2.0), "30% + 2cm");
test_value(Fractional::one() * 7.55, "7.55fr"); test_value(Fractional::one() * 7.55, "7.55fr");
test_value(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101"); test_value(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101");
// Collections.
test_value("hello", r#""hello""#); test_value("hello", r#""hello""#);
test_value("\n", r#""\n""#); test_value("\n", r#""\n""#);
test_value("\\", r#""\\""#); test_value("\\", r#""\\""#);
@ -752,12 +755,15 @@ mod tests {
test_value(dict![], "(:)"); test_value(dict![], "(:)");
test_value(dict!["one" => 1], "(one: 1)"); test_value(dict!["one" => 1], "(one: 1)");
test_value(dict!["two" => false, "one" => 1], "(one: 1, two: false)"); test_value(dict!["two" => false, "one" => 1], "(one: 1, two: false)");
test_value(Function::new(None, |_, _| Value::None), "<function>");
// Functions.
test_value(Function::new(None, |_, _| Ok(Value::None)), "<function>");
test_value( test_value(
Function::new(Some("nil".into()), |_, _| Value::None), Function::new(Some("nil".into()), |_, _| Ok(Value::None)),
"<function nil>", "<function nil>",
); );
// Dynamics.
test_value(Dynamic::new(1), "1"); test_value(Dynamic::new(1), "1");
test_value(Value::Error, "<error>");
} }
} }

View File

@ -1,5 +1,3 @@
use std::rc::Rc;
use super::*; use super::*;
/// A syntax node, encompassing a single logical entity of parsed source code. /// A syntax node, encompassing a single logical entity of parsed source code.
@ -52,7 +50,7 @@ pub struct HeadingNode {
/// The section depth (numer of equals signs). /// The section depth (numer of equals signs).
pub level: usize, pub level: usize,
/// The contents of the heading. /// The contents of the heading.
pub body: Rc<SyntaxTree>, pub body: SyntaxTree,
} }
/// An item in an unordered list: `- ...`. /// An item in an unordered list: `- ...`.

View File

@ -102,7 +102,7 @@ impl_visitors! {
} }
visit_heading(v, heading: HeadingNode) { visit_heading(v, heading: HeadingNode) {
v.visit_tree(r!(rc: heading.body)); v.visit_tree(r!(heading.body));
} }
visit_list(v, item: ListItem) { visit_list(v, item: ListItem) {

View File

@ -73,7 +73,7 @@ impl<T> SliceExt<T> for [T] {
} }
} }
/// This struct is produced by [`SliceExt::group_by_key`]. /// This struct is created by [`SliceExt::group_by_key`].
pub struct GroupByKey<'a, T, F> { pub struct GroupByKey<'a, T, F> {
slice: &'a [T], slice: &'a [T],
f: F, f: F,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
tests/ref/code/closure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -18,6 +18,7 @@
, rgb("002") , rgb("002")
,)} ,)}
---
// Error: 3 expected closing paren // Error: 3 expected closing paren
{(} {(}

View File

@ -1,38 +0,0 @@
// Test invalid code block syntax.
---
// Multiple unseparated expressions in one line.
// Error: 2-4 expected expression, found invalid token
{1u}
// Should output `1`.
// Error: 3 expected semicolon or line break
// Error: 4-5 cannot join integer with integer
{1 2}
// Should output `2`.
// Error: 12 expected semicolon or line break
// Error: 22 expected semicolon or line break
{let x = -1 let y = 3 x + y}
// Should output `3`.
{
// Error: 9-12 expected identifier, found string
for "v"
// Error: 10 expected keyword `in`
for v let z = 1 + 2
z
}
---
// Ref: false
// Error: 2:1 expected closing brace
{
---
// Ref: false
// Error: 1-2 unexpected closing brace
}

View File

@ -1,45 +0,0 @@
// Test scoping with blocks.
// Ref: false
---
// Block in template does not create a scope.
{ let x = 1 }
#test(x, 1)
---
// Block in expression does create a scope.
#let a = {
let b = 1
b
}
#test(a, 1)
// Error: 2-3 unknown variable
{b}
---
// Double block creates a scope.
{{
import b from "target.typ"
test(b, 1)
}}
// Error: 2-3 unknown variable
{b}
---
// Multiple nested scopes.
{
let a = "a1"
{
let a = "a2"
{
test(a, "a2")
let a = "a3"
test(a, "a3")
}
test(a, "a2")
}
test(a, "a1")
}

View File

@ -1,46 +1,55 @@
// Test code blocks. // Test code blocks.
// Ref: false
--- ---
All none // Ref: true
// Nothing evaluates to none. // Evaluates to join of none, [My ] and the two loop bodies.
{}
// Let evaluates to none.
{ let v = 0 }
// Type is joined with trailing none, evaluates to string.
{ {
type("") let parts = ("my fri", "end.")
none [Hello, ]
}
---
// Evaluates to single expression.
{ "Hello" }
// Evaluates to string.
{ let x = "Hel"; x + "lo" }
// Evaluates to join of none, [He] and the two loop bodies.
{
let parts = ("l", "lo")
[He]
for s in parts [{s}] for s in parts [{s}]
} }
---
// Evaluates to join of the templates and strings. // Evaluates to join of the templates and strings.
{ {
[Hey, ] [How]
if true { if true {
"there!" " are"
} }
[ ] [ ]
if false [Nope] if false [Nope]
[How are ] + "you?" [you] + "?"
} }
---
// Nothing evaluates to none.
#test({}, none)
// Let evaluates to none.
#test({ let v = 0 }, none)
// Evaluates to single expression.
#test({ "hello" }, "hello")
// Evaluates to string.
#test({ let x = "m"; x + "y" }, "my")
// Evaluated to int.
#test({
let x = 1
let y = 2
x + y
}, 3)
// String is joined with trailing none, evaluates to string.
#test({
type("")
none
}, "string")
---
// Some things can't be joined.
{ {
[A] [A]
// Error: 5-6 cannot join template with integer // Error: 5-6 cannot join template with integer
@ -49,10 +58,78 @@ All none
} }
--- ---
// Works the same way in code environment. // Block in template does not create a scope.
// Ref: false { let x = 1 }
#test(3, { #test(x, 1)
let x = 1
let y = 2 ---
x + y // Block in expression does create a scope.
}) #let a = {
let b = 1
b
}
#test(a, 1)
// Error: 2-3 unknown variable
{b}
---
// Double block creates a scope.
{{
import b from "target.typ"
test(b, 1)
}}
// Error: 2-3 unknown variable
{b}
---
// Multiple nested scopes.
{
let a = "a1"
{
let a = "a2"
{
test(a, "a2")
let a = "a3"
test(a, "a3")
}
test(a, "a2")
}
test(a, "a1")
}
---
// Multiple unseparated expressions in one line.
// Error: 2-4 expected expression, found invalid token
{1u}
// Should output `1`.
// Error: 3 expected semicolon or line break
{1 2}
// Should output `2`.
// Error: 12 expected semicolon or line break
// Error: 22 expected semicolon or line break
{let x = -1 let y = 3 x + y}
// Should output `3`.
{
// Error: 9-12 expected identifier, found string
for "v"
// Error: 10 expected keyword `in`
for v let z = 1 + 2
z
}
---
// Error: 2:1 expected closing brace
{
---
// Error: 1-2 unexpected closing brace
}

View File

@ -1,39 +0,0 @@
// Test invalid function calls.
---
// Error: 7-8 expected expression, found colon
#args(:)
// Error: 10-12 expected expression, found end of block comment
#args(a:1*/)
// Error: 8 expected comma
#args(1 2)
// Error: 7-8 expected identifier
// Error: 9 expected expression
#args(1:)
// Error: 7-8 expected identifier
#args(1:2)
// Error: 7-10 expected identifier
{args((x):1)}
---
#let x = "string"
// Error: 1-3 expected function, found string
#x()
// Error: 2:1 expected closing bracket
#args[`a]`
---
// Error: 7 expected closing paren
{args(}
---
// Error: 2:1 expected quote
// Error: 2:1 expected closing paren
#args("]

View File

@ -1,40 +0,0 @@
// Test wide calls.
---
// Test multiple wide calls in separate expressions.
#font!(fill: eastern) - First
#font!(fill: forest) - Second
---
// Test in heading.
= A #align!(right) B
C
---
// Test evaluation semantics.
#let x = 1
#let f(x, body) = (x, body)
#f!(x)
{ x = 2 }
---
// Test multiple wide calls in one expression.
// Ref: false
#let f() = []
#let g(x, y) = []
// Error: 2-4 wide calls are only allowed directly in templates
{f!()}
// Test nested wide calls.
// Error: 5-7 wide calls are only allowed directly in templates
#g!(f!())
---
// Test missing parentheses.
// Ref: false
// Error: 4 expected argument list
#f!

View File

@ -1,34 +1,64 @@
// Test function calls. // Test function calls.
---
// One argument.
#args(bold)
// One argument and trailing comma.
#args(1,)
// One named argument.
#args(a:2)
// Mixed arguments.
{args(1, b: "2", 3)}
// Should output `() + 2`.
#args() + 2
---
// Ref: false // Ref: false
---
// Ref: true
// Ommitted space.
#font(weight:bold)[Bold]
// Call return value of function with body.
#let f(x, body) = (y) => [#x] + body + [#y]
#f(1)[2](3)
// Don't parse this as a function.
// Should output `<function test> (it)`.
#test (it)
#let f(body) = body
#f[A]
#f()[A]
#f([A])
---
// Ref: true
// Test multiple wide calls in separate expressions inside a template.
[
#font!(fill: eastern) - First
#font!(fill: forest) - Second
]
// Test wide call in heading.
= A #align!(right) B
C
---
// Test wide call in expression.
// Error: 2-4 wide calls are only allowed directly in templates
{f!()}
// Error: 5-7 wide calls are only allowed directly in templates
#g!(f!())
---
// Test wide call evaluation semantics.
#let x = 1
#let f(x, body) = test(x, 1)
#f!(x)
{ x = 2 }
---
// Trailing comma.
#test(1 + 1, 2,)
// Call function assigned to variable. // Call function assigned to variable.
#let alias = type #let alias = type
#test(alias(alias), "function") #test(alias(alias), "function")
---
// Callee expressions. // Callee expressions.
{ {
// Error: 5-9 expected function, found boolean
true()
// Wrapped in parens. // Wrapped in parens.
test((type)("hi"), "string") test((type)("hi"), "string")
@ -37,30 +67,60 @@
test(adder(2)(5), 7) test(adder(2)(5), 7)
} }
#let f(x, body) = (y) => { ---
[{x}] + body + [{y}] // Error: 2-6 expected function, found boolean
} {true()}
// Call return value of function with body.
#f(1)[2](3)
// Don't allow this to be a closure.
// Should output `x => "hi"`.
#let x = "x"
#x => "hi"
--- ---
// Different forms of template arguments. #let x = "x"
#let a = "a" // Error: 1-3 expected function, found string
#x()
#args(a) \ ---
#args[a] \ #let f(x) = x
#args(a, [b])
// Template can be argument or body depending on whitespace. // Error: 1-6 expected function, found integer
#if "template" == type[b] [Sure ] #f(1)(2)
#if "template" == type [Nope.] #else [thing.]
// Should output `<function args> (Okay.)`. ---
#args (Okay.) #let f(x) = x
// Error: 1-6 expected function, found template
#f[1](2)
---
// Error: 7 expected argument list
#func!
// Error: 7-8 expected expression, found colon
#func(:)
// Error: 10-12 expected expression, found end of block comment
#func(a:1*/)
// Error: 8 expected comma
#func(1 2)
// Error: 7-8 expected identifier
// Error: 9 expected expression
#func(1:)
// Error: 7-8 expected identifier
#func(1:2)
// Error: 7-10 expected identifier
{func((x):1)}
---
// Error: 2:1 expected closing bracket
#func[`a]`
---
// Error: 7 expected closing paren
{func(}
---
// Error: 2:1 expected quote
// Error: 2:1 expected closing paren
#func("]

View File

@ -2,13 +2,22 @@
// Ref: false // Ref: false
--- ---
// Don't parse closure directly in template.
// Ref: true
#let x = "\"hi\""
// Should output `"hi" => "bye"`.
#x => "bye"
---
// Basic closure without captures. // Basic closure without captures.
{ {
let adder = (x, y) => x + y let adder = (x, y) => x + y
test(adder(2, 3), 5) test(adder(2, 3), 5)
} }
---
// Pass closure as argument and return closure. // Pass closure as argument and return closure.
// Also uses shorthand syntax for a single argument. // Also uses shorthand syntax for a single argument.
{ {
@ -19,6 +28,7 @@
test(h(2), 5) test(h(2), 5)
} }
---
// Capture environment. // Capture environment.
{ {
let mark = "?" let mark = "?"
@ -35,15 +45,7 @@
test(greet("Typst"), "Hi, Typst!") test(greet("Typst"), "Hi, Typst!")
} }
// Don't leak environment. ---
{
// Error: 18-19 unknown variable
let func() = x
let x = "hi"
test(func(), error)
}
// Redefined variable. // Redefined variable.
{ {
let x = 1 let x = 1
@ -54,6 +56,15 @@
test(f(), 3) test(f(), 3)
} }
---
// Don't leak environment.
{
// Error: 18-19 unknown variable
let func() = x
let x = "hi"
func()
}
--- ---
// Too few arguments. // Too few arguments.
{ {
@ -64,11 +75,11 @@
test(types("nope"), "[string, none]") test(types("nope"), "[string, none]")
} }
---
// Too many arguments. // Too many arguments.
{ {
let f(x) = x + 1 let f(x) = x + 1
// Error: 10-15 unexpected argument // Error: 10-15 unexpected argument
// Error: 17-24 unexpected argument
f(1, "two", () => x) f(1, "two", () => x)
} }

View File

@ -1,35 +0,0 @@
// Test for loop patterns.
// Ref: false
---
#let out = ()
// Values of array.
#for v in (1, 2, 3) {
out += (v,)
}
// Indices and values of array.
#for i, v in ("1", "2", "3") {
test(repr(i + 1), v)
}
// Values of dictionary.
#for v in (a: 4, b: 5) {
out += (v,)
}
// Keys and values of dictionary.
#for k, v in (a: 6, b: 7) {
out += (k,)
out += (v,)
}
#test(out, (1, 2, 3, 4, 5, "a", 6, "b", 7))
---
// Keys and values of strings.
// Error: 6-10 mismatched pattern
#for k, v in "hi" {
dont-care
}

View File

@ -1,42 +1,23 @@
// Test for loops. // Test for loops.
// Ref: false
--- ---
// Ref: true
// Empty array. // Empty array.
#for x in () [Nope] #for x in () [Nope]
// Array.
#let sum = 0
#for x in (1, 2, 3, 4, 5) {
sum += x
}
#test(sum, 15)
// Dictionary is not traversed in insertion order. // Dictionary is not traversed in insertion order.
// Should output `age: 1, name: Typst,`. // Should output `Age: 2. Name: Typst.`.
#for k, v in (Name: "Typst", Age: 2) [ #for k, v in (Name: "Typst", Age: 2) [
{k}: {v}. {k}: {v}.
] ]
// String.
{
let first = true
let out = for c in "abc" {
if not first {
", "
}
c
first = false
}
test(out, "a, b, c")
}
---
// Block body. // Block body.
// Should output `[1st, 2nd, 3rd, 4th, 5th, 6th]`. // Should output `[1st, 2nd, 3rd, 4th, 5th]`.
{ {
"[" "["
for v in (1, 2, 3, 4, 5, 6) { for v in (1, 2, 3, 4, 5) {
if v > 1 [, ] if v > 1 [, ]
[#v] [#v]
if v == 1 [st] if v == 1 [st]
@ -48,30 +29,60 @@
} }
// Template body. // Template body.
// Should output `234`. // Should output `2345`.
#for v in (1, 2, 3, 4, 5, 6, 7) [#if v >= 2 and v <= 5 { repr(v) }] #for v in (1, 2, 3, 4, 5, 6, 7) [#if v >= 2 and v <= 5 { repr(v) }]
--- ---
// Value of for loops. #let out = ()
// Ref: false
// Values of array.
#for v in (1, 2, 3) {
out += (v,)
}
// Indices and values of array.
#for i, v in ("1", "2", "3") {
test(repr(i + 1), v)
}
// Values of dictionary.
#for v in (a: 4, b: 5) {
out += (v,)
}
// Keys and values of dictionary.
#for k, v in (a: 6, b: 7) {
out += (k,)
out += (v,)
}
#test(out, (1, 2, 3, 4, 5, "a", 6, "b", 7))
// Chars of string.
#let first = true
#let joined = for c in "abc" {
if not first { ", " }
first = false
c
}
#test(joined, "a, b, c")
// Return value.
#test(for v in "" [], none) #test(for v in "" [], none)
#test(type(for v in "1" []), "template") #test(type(for v in "1" []), "template")
--- ---
// Ref: false
// Uniterable expression. // Uniterable expression.
// Error: 11-15 cannot loop over boolean // Error: 11-15 cannot loop over boolean
#for v in true {} #for v in true {}
// Make sure that we don't complain twice. ---
// Error: 11-18 cannot add integer and string // Keys and values of strings.
#for v in 1 + "2" {} // Error: 6-10 mismatched pattern
#for k, v in "hi" {
// Errors taint everything. dont-care
#test(error, for v in (1, 2, 3) { }
if v < 2 [Ok] else {error}
})
--- ---
// Error: 5 expected identifier // Error: 5 expected identifier
@ -89,19 +100,15 @@
// Error: 15 expected body // Error: 15 expected body
#for v in iter #for v in iter
// Should output `v in iter`.
// Error: 5 expected identifier // Error: 5 expected identifier
#for #for
v in iter {} v in iter {}
// Should output `A thing`.
// Error: 7-10 expected identifier, found string // Error: 7-10 expected identifier, found string
A#for "v" thing A#for "v" thing
// Should output `in iter`.
// Error: 6-9 expected identifier, found string // Error: 6-9 expected identifier, found string
#for "v" in iter {} #for "v" in iter {}
// Should output `+ b in iter`.
// Error: 7 expected keyword `in` // Error: 7 expected keyword `in`
#for a + b in iter {} #for a + b in iter {}

View File

@ -39,9 +39,16 @@
"Four" + point "Four" + point
} }
// Template can be argument or body depending on whitespace.
{
if "template" == type[b] [Fi] else [Nope]
if "template" == type [Nope] else [ve.]
}
--- ---
// Value of if expressions. // Value of if expressions.
// Ref: false // Ref: false
{ {
let x = 1 let x = 1
let y = 2 let y = 2
@ -61,13 +68,12 @@
} }
--- ---
// Ref: false
// Condition must be boolean. // Condition must be boolean.
// If it isn't, neither branch is evaluated. // If it isn't, neither branch is evaluated.
// Error: 5-14 expected boolean, found string // Error: 5-14 expected boolean, found string
#if "a" + "b" { nope } #else { nope } #if "a" + "b" { nope } #else { nope }
---
// Make sure that we don't complain twice. // Make sure that we don't complain twice.
// Error: 5-12 cannot add integer and string // Error: 5-12 cannot add integer and string
#if 1 + "2" {} #if 1 + "2" {}

View File

@ -11,8 +11,7 @@
#let value = [foo] #let value = [foo]
// Import multiple things. // Import multiple things.
// Error: 9-10 expected expression, found comma #import fn, value from "target.typ"
#import ,fn, value from "target.typ"
#fn[Like and Subscribe!] #fn[Like and Subscribe!]
#value #value
@ -24,10 +23,6 @@
#test(b, 1) #test(b, 1)
// This should not exist yet
// Error: 1-3 unknown variable
#d
// A wildcard import. // A wildcard import.
#import * from "target.typ" #import * from "target.typ"
@ -45,30 +40,35 @@
#import a, c, from "target.typ" #import a, c, from "target.typ"
--- ---
// Test bad imports.
// Ref: false
// Error: 19-21 file not found // Error: 19-21 file not found
#import name from "" #import name from ""
---
// Error: 16-27 file not found // Error: 16-27 file not found
#import * from "lib/0.2.1" #import * from "lib/0.2.1"
---
// Some non-text stuff. // Some non-text stuff.
// Error: 16-37 file is not valid utf-8 // Error: 16-37 file is not valid utf-8
#import * from "../../res/rhino.png" #import * from "../../res/rhino.png"
---
// Unresolved import. // Unresolved import.
// Error: 9-21 unresolved import // Error: 9-21 unresolved import
#import non_existing from "target.typ" #import non_existing from "target.typ"
// Cyclic import. ---
// Error: 16-41 cyclic import // Cyclic import of this very file.
#import * from "./importable/cycle1.typ" // Error: 16-30 cyclic import
#import * from "./import.typ"
--- ---
// Test bad syntax. // Cyclic import in other file.
#import * from "./importable/cycle1.typ"
This is never reached.
---
// Error: 8 expected import items // Error: 8 expected import items
// Error: 8 expected keyword `from` // Error: 8 expected keyword `from`
#import #import
@ -100,7 +100,6 @@
#from "target.typ" #from "target.typ"
// Should output `target`. // Should output `target`.
// Error: 1:16-2:2 file not found
// Error: 2:2 expected semicolon or line break // Error: 2:2 expected semicolon or line break
#import * from "target.typ #import * from "target.typ
"target "target

View File

@ -1,6 +1,5 @@
// Ref: false // Ref: false
// Error: 16-28 cyclic import
#import * from "cycle2.typ" #import * from "cycle2.typ"
#let inaccessible = "wow" #let inaccessible = "wow"

View File

@ -1,6 +1,5 @@
// Ref: false // Ref: false
// Error: 16-28 cyclic import
#import * from "cycle1.typ" #import * from "cycle1.typ"
#let val = "much cycle" #let val = "much cycle"

View File

@ -6,18 +6,21 @@
// Include a file // Include a file
#include "importable/chap1.typ" #include "importable/chap1.typ"
// The variables of the file should not appear in this scope.
// Error: 1-6 unknown variable
#name
// Expression as a file name. // Expression as a file name.
#let chap2 = include "import" + "able/chap" + "2.typ" #let chap2 = include "import" + "able/chap" + "2.typ"
-- _Intermission_ -- -- _Intermission_ --
#chap2 #chap2
---
{ {
// Expressions, code mode.
// Error: 21-43 file not found // Error: 21-43 file not found
let x = include "importable/chap3.typ" let x = include "importable/chap3.typ"
} }
---
#include "importable/chap1.typ"
// The variables of the file should not appear in this scope.
// Error: 1-6 unknown variable
#name

View File

@ -1,38 +1,19 @@
// Test let bindings. // Test let bindings.
--- ---
// Ref: false
// Automatically initialized with none. // Automatically initialized with none.
#let x #let x
#test(x, none) #test(x, none)
// Error: 9 expected expression
#let y =
#test(y, none)
// Manually initialized with one. // Manually initialized with one.
#let z = 1 #let z = 1
#test(z, 1) #test(z, 1)
---
// Syntax sugar for function definitions. // Syntax sugar for function definitions.
#let fill = conifer #let fill = conifer
#let rect(body) = rect(width: 2cm, fill: fill, pad(5pt, body)) #let rect(body) = rect(width: 2cm, fill: fill, pad(5pt, body))
#rect[Hi!] #rect[Hi!]
// Error: 13 expected body
#let func(x)
// Error: 2-6 unknown variable
{func}
// Error: 15 expected expression
#let func(x) =
// Error: 2-6 unknown variable
{func}
--- ---
// Termination. // Termination.
@ -47,20 +28,9 @@ One
#let v3 = 3; #let v3 = 3;
Three Three
// Terminated because expression ends.
// Error: 12 expected semicolon or line break
#let v4 = 4 Four
// Terminated by semicolon even though we are in a paren group.
// Error: 19 expected expression
// Error: 19 expected closing paren
#let v5 = (1, 2 + ; Five
#test(v1, 1) #test(v1, 1)
#test(v2, 2) #test(v2, 2)
#test(v3, 3) #test(v3, 3)
#test(v4, 4)
#test(v5, (1, 2))
--- ---
// Error: 5 expected identifier // Error: 5 expected identifier
@ -72,13 +42,27 @@ Three
// Error: 6-9 expected identifier, found string // Error: 6-9 expected identifier, found string
#let "v" #let "v"
// Should output `1`.
// Error: 7 expected semicolon or line break // Error: 7 expected semicolon or line break
#let v 1 #let v 1
// Error: 9 expected expression // Error: 9 expected expression
#let v = #let v =
// Should output a heading `1`.
// Error: 6-9 expected identifier, found string // Error: 6-9 expected identifier, found string
#let "v" = 1 #let "v" = 1
// Terminated because expression ends.
// Error: 12 expected semicolon or line break
#let v4 = 4 Four
// Terminated by semicolon even though we are in a paren group.
// Error: 19 expected expression
// Error: 19 expected closing paren
#let v5 = (1, 2 + ; Five
---
// Error: 13 expected body
#let func(x)
// Error: 15 expected expression
#let func(x) =

View File

@ -1,67 +1,84 @@
// Test invalid expressions. // Test invalid operations.
// Ref: false // Ref: false
--- ---
// Missing expressions.
// Error: 3 expected expression // Error: 3 expected expression
{-} {-}
---
// Error: 10 expected expression // Error: 10 expected expression
#test({1+}, 1) #test({1+}, 1)
---
// Error: 10 expected expression // Error: 10 expected expression
#test({2*}, 2) #test({2*}, 2)
--- ---
// Mismatched types.
// Error: 2-12 cannot apply '+' to template // Error: 2-12 cannot apply '+' to template
{+([] + [])} {+([] + [])}
---
// Error: 2-5 cannot apply '-' to string // Error: 2-5 cannot apply '-' to string
{-""} {-""}
---
// Error: 2-8 cannot apply 'not' to array // Error: 2-8 cannot apply 'not' to array
{not ()} {not ()}
---
// Error: 2-12 cannot apply '<=' to relative and relative // Error: 2-12 cannot apply '<=' to relative and relative
{30% <= 40%} {30% <= 40%}
---
// Special messages for +, -, * and /. // Special messages for +, -, * and /.
// Error: 03-10 cannot add integer and string // Error: 03-10 cannot add integer and string
// Error: 12-19 cannot subtract integer from relative {(1 + "2", 40% - 1)}
// Error: 21-29 cannot multiply integer with boolean
// Error: 31-39 cannot divide integer by length
{(1 + "2", 40% - 1, 2 * true, 3 / 12pt)}
// Error: 14-22 cannot apply '+=' to integer and string ---
// Error: 12-19 cannot subtract integer from relative
{(1234567, 40% - 1)}
---
// Error: 2-10 cannot multiply integer with boolean
{2 * true}
---
// Error: 2-10 cannot divide integer by length
{3 / 12pt}
---
// Error: 14-22 cannot add integer and string
{ let x = 1; x += "2" } { let x = 1; x += "2" }
---
// Error: 13-14 expected argument list, found integer // Error: 13-14 expected argument list, found integer
{ test with 2 } { test with 2 }
---
// Error: 3-4 expected function, found integer // Error: 3-4 expected function, found integer
{ 1 with () } { 1 with () }
---
// Error: 3-10 cannot apply '..' to integer and string // Error: 3-10 cannot apply '..' to integer and string
{ 1 .. "" } { 1 .. "" }
--- ---
// Bad left-hand sides of assignment.
// Error: 3-6 cannot assign to this expression // Error: 3-6 cannot assign to this expression
{ (x) = "" } { (x) = "" }
---
// Error: 3-8 cannot assign to this expression // Error: 3-8 cannot assign to this expression
{ 1 + 2 += 3 } { 1 + 2 += 3 }
---
// Error: 3-4 unknown variable // Error: 3-4 unknown variable
{ z = 1 } { z = 1 }
---
// Error: 3-7 cannot assign to a constant // Error: 3-7 cannot assign to a constant
{ rect = "hi" } { rect = "hi" }
---
// Works if we define rect beforehand // Works if we define rect beforehand
// (since then it doesn't resolve to the standard library version anymore). // (since then it doesn't resolve to the standard library version anymore).
#let rect = "" #let rect = ""

View File

@ -10,9 +10,6 @@
{ke-bab} \ {ke-bab} \
{α} {α}
// Error: 2-3 unknown variable
{_}
--- ---
// Literal values. // Literal values.
{none} (empty) \ {none} (empty) \

View File

@ -29,23 +29,16 @@
#test(type(while i < 1 [{ i += 1 }]), "template") #test(type(while i < 1 [{ i += 1 }]), "template")
--- ---
// Ref: false
// Condition must be boolean. // Condition must be boolean.
// Error: 8-14 expected boolean, found template // Error: 8-14 expected boolean, found template
#while [nope] [nope] #while [nope] [nope]
// Make sure that we don't complain twice. ---
// Error: 8-15 unknown variable // Make sure that we terminate and don't complain multiple times.
#while nothing {} #while true {
// Error: 5-9 unknown variable
// Errors taint everything. nope
#let i = 0 }
#test(error, while i < 10 {
i += 1
if i < 5 [nope] else { error }
})
#test(i, 10)
--- ---
// Error: 7 expected expression // Error: 7 expected expression
@ -57,11 +50,9 @@
// Error: 9 expected body // Error: 9 expected body
#while x #while x
// Should output `x`.
// Error: 7 expected expression // Error: 7 expected expression
#while #while
x {} x {}
// Should output `something`.
// Error: 9 expected body // Error: 9 expected body
#while x something #while x something

View File

@ -39,9 +39,9 @@ Expanded by height.
--- ---
// Radius wins over width and height. // Radius wins over width and height.
// Error: 23-34 unexpected argument // Error: 23-34 unexpected argument
// Error: 36-49 unexpected argument
#circle(radius: 10pt, width: 50pt, height: 100pt, fill: eastern) #circle(radius: 10pt, width: 50pt, height: 100pt, fill: eastern)
---
// Width wins over height. // Width wins over height.
// Error: 22-34 unexpected argument // Error: 9-21 unexpected argument
#circle(width: 20pt, height: 50pt, fill: eastern) #circle(height: 50pt, width: 20pt, fill: eastern)

View File

@ -10,12 +10,6 @@
// Load an RGB JPEG image. // Load an RGB JPEG image.
#image("../../res/tiger.jpg") #image("../../res/tiger.jpg")
// Error: 8-29 file not found
#image("path/does/not/exist")
// Error: 8-21 failed to load image
#image("./image.typ")
--- ---
// Test configuring the size and fitting behaviour of images. // Test configuring the size and fitting behaviour of images.
@ -36,3 +30,11 @@
// Make sure the bounding-box of the image is correct. // Make sure the bounding-box of the image is correct.
#align(bottom, right, image("../../res/tiger.jpg", width: 60pt)) #align(bottom, right, image("../../res/tiger.jpg", width: 60pt))
---
// Error: 8-29 file not found
#image("path/does/not/exist")
---
// Error: 8-21 failed to load image
#image("./image.typ")

View File

@ -9,12 +9,6 @@ Auto-sized square. \
Typst Typst
] ]
---
// Length wins over width and height.
// Error: 09-20 unexpected argument
// Error: 22-34 unexpected argument
#square(width: 10cm, height: 20cm, length: 1cm, fill: rgb("eb5278"))
--- ---
// Test height overflow. // Test height overflow.
#page!(width: 75pt, height: 100pt) #page!(width: 75pt, height: 100pt)
@ -28,3 +22,8 @@ Auto-sized square. \
#square(fill: conifer)[ #square(fill: conifer)[
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]
---
// Length wins over width and height.
// Error: 09-20 unexpected argument
#square(width: 10cm, height: 20cm, length: 1cm, fill: rgb("eb5278"))

View File

@ -25,9 +25,9 @@
gutter-columns: (0pt, 10%), gutter-columns: (0pt, 10%),
image("../../res/rhino.png"), image("../../res/rhino.png"),
align(right, rect(width: 100%, fill: eastern)[LoL]), align(right, rect(width: 100%, fill: eastern)[LoL]),
"rofl", [rofl],
"\nA" * 3, [\ A] * 3,
"Ha!\n" * 3, [Ha!\ ] * 3,
) )
--- ---
@ -38,9 +38,9 @@
gutter-rows: (8pt,), gutter-rows: (8pt,),
gutter-columns: (0pt, 10%), gutter-columns: (0pt, 10%),
[A], [B], [C], [A], [B], [C],
"Ha!\n" * 6, [Ha!\ ] * 6,
"rofl", [rofl],
"\nA" * 3, [\ A] * 3,
[hello], [hello],
[darkness], [darkness],
[my old] [my old]
@ -54,10 +54,10 @@
gutter-rows: (10pt,), gutter-rows: (10pt,),
gutter-columns: (0pt, 10%), gutter-columns: (0pt, 10%),
[A], [B], [C], [D], [A], [B], [C], [D],
grid(columns: 2, [A], [B], "C\n"*3, [D]), grid(columns: 2, [A], [B], [C\ ]*3, [D]),
align(right, rect(width: 100%, fill: eastern)[LoL]), align(right, rect(width: 100%, fill: eastern)[LoL]),
"rofl", [rofl],
"E\n"*4, [E\ ]*4,
) )
--- ---

View File

@ -10,8 +10,7 @@
#rect(width: 20pt, height: 20pt, fill: rgb("eb5278")) #rect(width: 20pt, height: 20pt, fill: rgb("eb5278"))
] ]
// Error: 13-23 missing argument: body Hi #box(pad(left: 10pt)[]) there
Hi #box(pad(left: 10pt)) there
--- ---
#let pad(body) = pad(left: 10pt, right: 10pt, body) #let pad(body) = pad(left: 10pt, right: 10pt, body)

View File

@ -22,9 +22,6 @@
// Ensure that specific margins override general margins. // Ensure that specific margins override general margins.
#page(margins: 0pt, left: 20pt)[Overriden] #page(margins: 0pt, left: 20pt)[Overriden]
// Error: 8-19 unknown variable
#page!(nonexistant)
// Flipped predefined paper. // Flipped predefined paper.
#page("a11", flip: true)[Flipped A11] #page("a11", flip: true)[Flipped A11]

View File

@ -10,10 +10,7 @@ First of two
A A
#box[ #box[
B B
// Error: 16 cannot modify page from here
#pagebreak() #pagebreak()
// Error: 11-15 cannot modify page from here
#page("a4")[] #page("a4")[]
] ]
C C

View File

@ -13,6 +13,7 @@ Add #h(10pt) #h(10pt) up
// Relative to font size. // Relative to font size.
Relative #h(100%) spacing Relative #h(100%) spacing
---
// Missing spacing. // Missing spacing.
// Error: 12 missing argument: spacing // Error: 12 missing argument: spacing
Totally #h() ignored Totally #h() ignored

View File

@ -21,14 +21,15 @@
// Escaped escape sequence. // Escaped escape sequence.
\u{41} vs. \\u\{41\} \u{41} vs. \\u\{41\}
// Some code stuff in text.
let f() , ; : | + - /= == 12 "string"
---
// Unicode codepoint does not exist. // Unicode codepoint does not exist.
// Error: 1-11 invalid unicode escape sequence // Error: 1-11 invalid unicode escape sequence
\u{FFFFFF} \u{FFFFFF}
---
// Unterminated. // Unterminated.
// Error: 6 expected closing brace // Error: 6 expected closing brace
\u{41*Bold* \u{41*Bold*
---
// Some code stuff in text.
let f() , ; : | + - /= == 12 "string"

View File

@ -1,19 +1,34 @@
// Test text decorations. // Test text decorations.
--- ---
#strike[Statements dreamt up by the utterly deranged.] // Basic strikethrough.
#strike[
Sometimes, we work #strike(10pt, extent: 5%)[in secret]. Statements dreamt up by the utterly deranged.
There might be #strike(stroke: rgb("abcdef88"), thickness: 10pt, extent: 5%)[redacted] ]
things.
// Move underline down.
#underline(offset: 5pt)[Further below.] #underline(offset: 5pt)[Further below.]
--- // Different color.
#underline(rgb("fc0030"))[Critical information is conveyed here.] #underline(rgb("fc0030"))[Critical information is conveyed here.]
#underline[Still important, but not #underline(0pt)[mission ]critical.]
// Inherits font color.
#font(fill: rgb("fc0030"), underline[Change with the wind.]) #font(fill: rgb("fc0030"), underline[Change with the wind.])
--- // Both over- and underline.
#overline(underline[Running amongst the wolves.]) #overline(underline[Running amongst the wolves.])
// Disable underline by setting it back to 0pt.
#underline[Still important, but not #underline(0pt)[mission ]critical.]
---
#let redact = strike with (10pt, extent: 5%)
#let highlight = strike with (
stroke: rgb("abcdef88"),
thickness: 10pt,
extent: 5%,
)
// Abuse thickness and transparency for redacting and highlighting stuff.
Sometimes, we work #redact[in secret].
There might be #highlight[redacted] things.

View File

@ -54,21 +54,25 @@ Emoji: 🐪, 🌋, 🏞
#font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] #font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.]
--- ---
// Ref: false
// Error: 7-12 unexpected argument // Error: 7-12 unexpected argument
#font(false)[] #font(false)[]
---
// Error: 14-18 expected font style, found font weight // Error: 14-18 expected font style, found font weight
// Error: 28-34 expected font weight, found string #font(style: bold, weight: "thin")[]
// Error: 43-44 expected string or array of strings, found integer
#font(style: bold, weight: "thin", serif: 0)[]
---
// Error: 14-15 expected string or array of strings, found integer
#font(serif: 0)[]
---
// Error: 19-23 unexpected argument
#font(size: 10pt, 12pt)[]
---
// Error: 28-35 unexpected argument
#font(family: "Helvetica", "Arial")[]
---
// Error: 7-27 unexpected argument // Error: 7-27 unexpected argument
#font(something: "invalid")[] #font(something: "invalid")[]
// Error: 13-23 unexpected argument
#font(12pt, size: 10pt)[]
// Error: 16-35 unexpected argument
#font("Arial", family: "Helvetica")[]

View File

@ -2,16 +2,12 @@
--- ---
// Spacing around let. // Spacing around let.
// Error: 6 expected identifier
A#let;B \
A#let x = 1;B #test(x, 1) \ A#let x = 1;B #test(x, 1) \
A #let x = 2;B #test(x, 2) \ A #let x = 2;B #test(x, 2) \
A#let x = 3; B #test(x, 3) A#let x = 3; B #test(x, 3)
--- ---
// Spacing around if-else. // Spacing around if-else.
A#if true [B]C \ A#if true [B]C \
A#if true [B] C \ A#if true [B] C \
A #if true{"B"}C \ A #if true{"B"}C \
@ -21,7 +17,6 @@ A#if true [B] #else [] C
--- ---
// Spacing around while loop. // Spacing around while loop.
#let c = true; A#while c [{c = false}B]C \ #let c = true; A#while c [{c = false}B]C \
#let c = true; A#while c [{c = false}B] C \ #let c = true; A#while c [{c = false}B] C \
#let c = true; A #while c { c = false; "B" }C \ #let c = true; A #while c { c = false; "B" }C \
@ -29,7 +24,6 @@ A#if true [B] #else [] C
--- ---
// Spacing around for loop. // Spacing around for loop.
A#for _ in (none,) [B]C \ A#for _ in (none,) [B]C \
A#for _ in (none,) [B] C \ A#for _ in (none,) [B] C \
A #for _ in (none,) {"B"}C A #for _ in (none,) {"B"}C

View File

@ -3,14 +3,15 @@
--- ---
// Test the `len` function. // Test the `len` function.
#test(len(()), 0) #test(len(()), 0)
#test(len(("A", "B", "C")), 3) #test(len(("A", "B", "C")), 3)
#test(len("Hello World!"), 12) #test(len("Hello World!"), 12)
#test(len((a: 1, b: 2)), 2) #test(len((a: 1, b: 2)), 2)
---
// Error: 6 missing argument: collection // Error: 6 missing argument: collection
#len() #len()
---
// Error: 6-10 expected string, array or dictionary // Error: 6-10 expected string, array or dictionary
#len(12pt) #len(12pt)

View File

@ -11,13 +11,14 @@
// Clamped. // Clamped.
#test(rgb(-30, 15.5, 0.5), rgb("00ff80")) #test(rgb(-30, 15.5, 0.5), rgb("00ff80"))
// Error: 11-15 missing argument: blue component ---
#test(rgb(0, 1), rgb("00ff00")) // Error: 6-11 invalid color
#rgb("lol")
// Error: 11-16 invalid color ---
#test(rgb("lol"), error) // Error: 6 missing argument: red component
#rgb()
// Error: 11 missing argument: red component ---
// Error: 11 missing argument: green component // Error: 6-10 missing argument: blue component
// Error: 11 missing argument: blue component #rgb(0, 1)
#test(rgb(), black)

View File

@ -8,8 +8,10 @@
#test(max(-3, 11), 11) #test(max(-3, 11), 11)
#test(min("hi"), "hi") #test(min("hi"), "hi")
---
// Error: 6 missing argument: value // Error: 6 missing argument: value
#min() #min()
---
// Error: 11-18 cannot compare integer with string // Error: 11-18 cannot compare integer with string
#test(min(1, "hi"), error) #test(min(1, "hi"), error)

View File

@ -1,4 +1,3 @@
use std::cell::RefCell;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
@ -11,20 +10,17 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use walkdir::WalkDir; use walkdir::WalkDir;
use typst::color::Color; use typst::color::Color;
use typst::diag::{Diag, DiagSet, Level}; use typst::diag::{Error, TypResult};
use typst::eval::{eval, Scope, Value}; use typst::eval::{eval, Value};
use typst::exec::{exec, State}; use typst::exec::{exec, State};
use typst::geom::{self, Length, PathElement, Point, Sides, Size}; use typst::geom::{self, Length, PathElement, Point, Sides, Size};
use typst::image::ImageId; use typst::image::ImageId;
use typst::layout::{layout, Element, Frame, Geometry, Paint, Text}; use typst::layout::{layout, Element, Frame, Geometry, LayoutTree, Paint, Text};
use typst::loading::{FileId, FsLoader}; use typst::loading::{FileId, FsLoader};
use typst::parse::{parse, LineMap, Scanner}; use typst::parse::{parse, LineMap, Scanner};
use typst::syntax::{Location, Pos}; use typst::syntax::{Location, Pos};
use typst::Context; use typst::Context;
#[cfg(feature = "layout-cache")]
use typst::layout::LayoutTree;
const TYP_DIR: &str = "./typ"; const TYP_DIR: &str = "./typ";
const REF_DIR: &str = "./ref"; const REF_DIR: &str = "./ref";
const PNG_DIR: &str = "./png"; const PNG_DIR: &str = "./png";
@ -67,10 +63,22 @@ fn main() {
page.size = Size::new(Length::pt(120.0), Length::inf()); page.size = Size::new(Length::pt(120.0), Length::inf());
page.margins = Sides::splat(Some(Length::pt(10.0).into())); page.margins = Sides::splat(Some(Length::pt(10.0).into()));
// We hook up some extra test helpers into the global scope. // Hook up an assert function into the global scope.
let mut std = typst::library::new(); let mut std = typst::library::new();
let panics = Rc::new(RefCell::new(vec![])); std.def_func("test", move |_, args| {
register_helpers(&mut std, Rc::clone(&panics)); let lhs = args.expect::<Value>("left-hand side")?;
let rhs = args.expect::<Value>("right-hand side")?;
if lhs != rhs {
typst::bail!(
args.file,
args.span,
"Assertion failed: {:?} != {:?}",
lhs,
rhs
);
}
Ok(Value::None)
});
// Create loader and context. // Create loader and context.
let loader = FsLoader::new().with_path(FONT_DIR).wrap(); let loader = FsLoader::new().with_path(FONT_DIR).wrap();
@ -88,7 +96,6 @@ fn main() {
ok &= test( ok &= test(
&mut ctx, &mut ctx,
loader.as_ref(), loader.as_ref(),
&panics,
&src_path, &src_path,
&png_path, &png_path,
&ref_path, &ref_path,
@ -134,35 +141,9 @@ impl Args {
} }
} }
type Panics = Rc<RefCell<Vec<Panic>>>;
struct Panic {
pos: Pos,
lhs: Option<Value>,
rhs: Option<Value>,
}
fn register_helpers(scope: &mut Scope, panics: Rc<RefCell<Vec<Panic>>>) {
scope.def_const("error", Value::Error);
scope.def_func("args", |_, args| {
let repr = typst::pretty::pretty(args);
args.items.clear();
Value::template(move |ctx| ctx.push_monospace_text(&repr))
});
scope.def_func("test", move |ctx, args| {
let lhs = args.expect::<Value>(ctx, "left-hand side");
let rhs = args.expect::<Value>(ctx, "right-hand side");
if lhs != rhs {
panics.borrow_mut().push(Panic { pos: args.span.start, lhs, rhs });
}
Value::None
});
}
fn test( fn test(
ctx: &mut Context, ctx: &mut Context,
loader: &FsLoader, loader: &FsLoader,
panics: &Panics,
src_path: &Path, src_path: &Path,
png_path: &Path, png_path: &Path,
ref_path: &Path, ref_path: &Path,
@ -171,8 +152,8 @@ fn test(
let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path); let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
println!("Testing {}", name.display()); println!("Testing {}", name.display());
let file = loader.resolve(src_path).unwrap();
let src = fs::read_to_string(src_path).unwrap(); let src = fs::read_to_string(src_path).unwrap();
let src_id = loader.resolve(src_path).unwrap();
let mut ok = true; let mut ok = true;
let mut frames = vec![]; let mut frames = vec![];
@ -196,7 +177,7 @@ fn test(
} }
} else { } else {
let (part_ok, compare_here, part_frames) = let (part_ok, compare_here, part_frames) =
test_part(ctx, panics, src_id, part, i, compare_ref, lines); test_part(ctx, file, part, i, compare_ref, lines);
ok &= part_ok; ok &= part_ok;
compare_ever |= compare_here; compare_ever |= compare_here;
frames.extend(part_frames); frames.extend(part_frames);
@ -221,7 +202,7 @@ fn test(
println!(" Does not match reference image. ❌"); println!(" Does not match reference image. ❌");
ok = false; ok = false;
} }
} else { } else if !frames.is_empty() {
println!(" Failed to open reference image. ❌"); println!(" Failed to open reference image. ❌");
ok = false; ok = false;
} }
@ -236,71 +217,58 @@ fn test(
fn test_part( fn test_part(
ctx: &mut Context, ctx: &mut Context,
panics: &Panics, file: FileId,
src_id: FileId,
src: &str, src: &str,
i: usize, i: usize,
compare_ref: bool, compare_ref: bool,
lines: u32, lines: u32,
) -> (bool, bool, Vec<Rc<Frame>>) { ) -> (bool, bool, Vec<Rc<Frame>>) {
let map = LineMap::new(src); let map = LineMap::new(src);
let (local_compare_ref, ref_diags) = parse_metadata(src, &map); let (local_compare_ref, mut ref_errors) = parse_metadata(file, src, &map);
let compare_ref = local_compare_ref.unwrap_or(compare_ref); let compare_ref = local_compare_ref.unwrap_or(compare_ref);
// Clear the module cache between tests.
ctx.modules.clear();
let ast = parse(src);
let module = eval(ctx, src_id, Rc::new(ast.output));
let tree = exec(ctx, &module.output.template);
let mut frames = layout(ctx, &tree.output);
let mut diags = ast.diags;
diags.extend(module.diags);
diags.extend(tree.diags);
let mut ok = true; let mut ok = true;
for panic in panics.borrow().iter() { let result = typeset(ctx, file, src);
let line = map.location(panic.pos).unwrap().line; let (frames, mut errors) = match result {
println!(" Assertion failed in line {}", lines + line); #[allow(unused_variables)]
if let (Some(lhs), Some(rhs)) = (&panic.lhs, &panic.rhs) { Ok((tree, mut frames)) => {
println!(" Left: {:?}", lhs); #[cfg(feature = "layout-cache")]
println!(" Right: {:?}", rhs); (ok &= test_incremental(ctx, i, &tree, &frames));
} else {
println!(" Missing argument."); if !compare_ref {
frames.clear();
}
(frames, vec![])
} }
ok = false; Err(errors) => (vec![], *errors),
} };
panics.borrow_mut().clear(); // TODO: Also handle errors from other files.
errors.retain(|error| error.file == file);
ref_errors.sort();
errors.sort();
if diags != ref_diags { if errors != ref_errors {
println!(" Subtest {} does not match expected diagnostics. ❌", i); println!(" Subtest {} does not match expected errors. ❌", i);
ok = false; ok = false;
for diag in &diags { for error in errors.iter() {
if !ref_diags.contains(diag) { if error.file == file && !ref_errors.contains(error) {
print!(" Not annotated | "); print!(" Not annotated | ");
print_diag(diag, &map, lines); print_error(error, &map, lines);
} }
} }
for diag in &ref_diags { for error in ref_errors.iter() {
if !diags.contains(diag) { if !errors.contains(error) {
print!(" Not emitted | "); print!(" Not emitted | ");
print_diag(diag, &map, lines); print_error(error, &map, lines);
} }
} }
} }
#[cfg(feature = "layout-cache")]
(ok &= test_incremental(ctx, i, &tree.output, &frames));
if !compare_ref {
frames.clear();
}
(ok, compare_ref, frames) (ok, compare_ref, frames)
} }
@ -350,9 +318,9 @@ fn test_incremental(
ok ok
} }
fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) { fn parse_metadata(file: FileId, src: &str, map: &LineMap) -> (Option<bool>, Vec<Error>) {
let mut diags = DiagSet::new();
let mut compare_ref = None; let mut compare_ref = None;
let mut errors = vec![];
let lines: Vec<_> = src.lines().map(str::trim).collect(); let lines: Vec<_> = src.lines().map(str::trim).collect();
for (i, line) in lines.iter().enumerate() { for (i, line) in lines.iter().enumerate() {
@ -364,10 +332,8 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {
compare_ref = Some(true); compare_ref = Some(true);
} }
let (level, rest) = if let Some(rest) = line.strip_prefix("// Warning: ") { let rest = if let Some(rest) = line.strip_prefix("// Error: ") {
(Level::Warning, rest) rest
} else if let Some(rest) = line.strip_prefix("// Error: ") {
(Level::Error, rest)
} else { } else {
continue; continue;
}; };
@ -391,18 +357,30 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {
let start = pos(&mut s); let start = pos(&mut s);
let end = if s.eat_if('-') { pos(&mut s) } else { start }; let end = if s.eat_if('-') { pos(&mut s) } else { start };
diags.insert(Diag::new(start .. end, level, s.rest().trim())); errors.push(Error::new(file, start .. end, s.rest().trim()));
} }
(compare_ref, diags) (compare_ref, errors)
} }
fn print_diag(diag: &Diag, map: &LineMap, lines: u32) { fn typeset(
let mut start = map.location(diag.span.start).unwrap(); ctx: &mut Context,
let mut end = map.location(diag.span.end).unwrap(); file: FileId,
src: &str,
) -> TypResult<(LayoutTree, Vec<Rc<Frame>>)> {
let ast = parse(file, src)?;
let module = eval(ctx, file, Rc::new(ast))?;
let tree = exec(ctx, &module.template);
let frames = layout(ctx, &tree);
Ok((tree, frames))
}
fn print_error(error: &Error, map: &LineMap, lines: u32) {
let mut start = map.location(error.span.start).unwrap();
let mut end = map.location(error.span.end).unwrap();
start.line += lines; start.line += lines;
end.line += lines; end.line += lines;
println!("{}: {}-{}: {}", diag.level, start, end, diag.message); println!("Error: {}-{}: {}", start, end, error.message);
} }
fn draw(ctx: &Context, frames: &[Rc<Frame>], dpi: f32) -> sk::Pixmap { fn draw(ctx: &Context, frames: &[Rc<Frame>], dpi: f32) -> sk::Pixmap {