diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-07-30 18:04:08 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-07-30 18:49:19 +0200 |
| commit | 1ee1d078e2480ddd08d40915bc7a74a8352acff0 (patch) | |
| tree | 1e7ff367278a19fead3e404cf06d65bfb80a6cd9 | |
| parent | 42a27b48df427edf8dbb624c51551a90ecf2e7ea (diff) | |
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
84 files changed, 1307 insertions, 1464 deletions
diff --git a/bench/src/clock.rs b/bench/src/clock.rs index c3c5b378..7cd32711 100644 --- a/bench/src/clock.rs +++ b/bench/src/clock.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use criterion::{criterion_group, criterion_main, Criterion}; -use typst::diag::Pass; use typst::eval::{eval, Module}; use typst::exec::exec; use typst::export::pdf; @@ -25,9 +24,9 @@ fn benchmarks(c: &mut Criterion) { for case in CASES { let path = Path::new(TYP_DIR).join(case); 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 case = Case::new(src_id, src, ctx.clone()); + let case = Case::new(file, src, ctx.clone()); macro_rules! bench { ($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. struct Case { ctx: Rc<RefCell<Context>>, - src_id: FileId, + file: FileId, src: String, ast: Rc<SyntaxTree>, module: Module, @@ -89,16 +88,16 @@ struct 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 ast = Rc::new(parse(&src).output); - let module = eval(&mut borrowed, src_id, Rc::clone(&ast)).output; - let tree = exec(&mut borrowed, &module.template).output; + let ast = Rc::new(parse(file, &src).unwrap()); + let module = eval(&mut borrowed, file, Rc::clone(&ast)).unwrap(); + let tree = exec(&mut borrowed, &module.template); let frames = layout(&mut borrowed, &tree); drop(borrowed); Self { ctx, - src_id, + file, src, ast, module, @@ -108,18 +107,14 @@ impl Case { } fn parse(&self) -> SyntaxTree { - parse(&self.src).output + parse(self.file, &self.src).unwrap() } - fn eval(&self) -> Pass<Module> { - eval( - &mut self.ctx.borrow_mut(), - self.src_id, - Rc::clone(&self.ast), - ) + fn eval(&self) -> Module { + eval(&mut self.ctx.borrow_mut(), self.file, Rc::clone(&self.ast)).unwrap() } - fn exec(&self) -> Pass<LayoutTree> { + fn exec(&self) -> LayoutTree { exec(&mut self.ctx.borrow_mut(), &self.module.template) } @@ -127,8 +122,8 @@ impl Case { layout(&mut self.ctx.borrow_mut(), &self.tree) } - fn typeset(&self) -> Pass<Vec<Rc<Frame>>> { - self.ctx.borrow_mut().typeset(self.src_id, &self.src) + fn typeset(&self) -> Vec<Rc<Frame>> { + self.ctx.borrow_mut().typeset(self.file, &self.src).unwrap() } fn pdf(&self) -> Vec<u8> { diff --git a/bench/src/parsing.rs b/bench/src/parsing.rs index d34faf62..d9064a63 100644 --- a/bench/src/parsing.rs +++ b/bench/src/parsing.rs @@ -1,6 +1,7 @@ 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::syntax::SyntaxTree; @@ -30,8 +31,8 @@ fn bench_tokenize() -> usize { Tokens::new(black_box(SRC), black_box(TokenMode::Markup)).count() } -fn bench_parse() -> Pass<SyntaxTree> { - parse(black_box(SRC)) +fn bench_parse() -> TypResult<SyntaxTree> { + parse(FileId::from_raw(0), black_box(SRC)) } main!(bench_decode, bench_scan, bench_tokenize, bench_parse); diff --git a/src/diag.rs b/src/diag.rs index 6158b5c0..76d7c6b7 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -1,113 +1,70 @@ -//! Diagnostics for source code. -//! -//! Errors are never fatal, the document will always compile and yield a layout. - -use std::collections::BTreeSet; -use std::fmt::{self, Display, Formatter}; +//! Diagnostics. use serde::{Deserialize, Serialize}; +use crate::loading::FileId; use crate::syntax::Span; -/// The result of some pass: Some output `T` and diagnostics. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -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> { - /// Create a new pass from output and diagnostics. - pub fn new(output: T, diags: DiagSet) -> Self { - Self { output, diags } - } -} +/// The result type for typesetting and all its subpasses. +pub type TypResult<T> = Result<T, Box<Vec<Error>>>; -/// A set of diagnostics. -/// -/// 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 result type with a string error message. +pub type StrResult<T> = Result<T, String>; -/// A diagnostic with severity level and message. +/// An error in a source file. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -pub struct Diag { - /// The source code location. +pub struct Error { + /// The file that contains the error. + pub file: FileId, + /// The erronous location in the source code. pub span: Span, - /// How severe / important the diagnostic is. - pub level: Level, - /// A message describing the diagnostic. + /// A diagnostic message describing the problem. pub message: String, } -impl Diag { - /// Create a new diagnostic from message and level. - pub fn new(span: impl Into<Span>, level: Level, message: impl Into<String>) -> Self { +impl Error { + /// Create a new, bare error. + pub fn new(file: FileId, span: impl Into<Span>, message: impl Into<String>) -> Self { Self { + file, span: span.into(), - level, message: message.into(), } } -} -impl Display for Diag { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}: {}", self.level, self.message) + /// Create a boxed vector containing one error. The return value is suitable + /// as the `Err` variant of a [`TypResult`]. + pub fn boxed( + file: FileId, + span: impl Into<Span>, + message: impl Into<String>, + ) -> Box<Vec<Self>> { + Box::new(vec![Self::new(file, span, message)]) } -} - -/// How severe / important a diagnostic is. -#[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", - }) + /// 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) } } -/// 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); -/// ``` +/// Early-return with a vec-boxed [`Error`]. #[macro_export] -macro_rules! error { - ($span:expr, $($tts:tt)*) => { - $crate::diag::Diag::new( - $span, - $crate::diag::Level::Error, - format!($($tts)*), - ) +macro_rules! bail { + ($file:expr, $span:expr, $message:expr $(,)?) => { + return Err(Box::new(vec![$crate::diag::Error::new( + $file, $span, $message, + )])); }; -} -/// 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)*), - ) + ($file:expr, $span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { + $crate::bail!($file, $span, format!($fmt, $($arg),+)); }; } diff --git a/src/eval/function.rs b/src/eval/function.rs index ca447a48..b9a168d2 100644 --- a/src/eval/function.rs +++ b/src/eval/function.rs @@ -3,8 +3,10 @@ use std::ops::Deref; use std::rc::Rc; 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::util::EcoString; /// An evaluatable function. #[derive(Clone)] @@ -16,13 +18,13 @@ struct Repr<T: ?Sized> { func: T, } -type Func = dyn Fn(&mut EvalContext, &mut FuncArgs) -> Value; +type Func = dyn Fn(&mut EvalContext, &mut FuncArgs) -> TypResult<Value>; impl Function { /// Create a new function from a rust closure. pub fn new<F>(name: Option<EcoString>, func: F) -> Self where - F: Fn(&mut EvalContext, &mut FuncArgs) -> Value + 'static, + F: Fn(&mut EvalContext, &mut FuncArgs) -> TypResult<Value> + 'static, { Self(Rc::new(Repr { name, func })) } @@ -57,6 +59,8 @@ impl PartialEq for Function { /// Evaluated arguments to a function. #[derive(Debug, Clone, PartialEq)] pub struct FuncArgs { + /// The file in which the function was called. + pub file: FileId, /// The span of the whole argument list. pub span: Span, /// The positional arguments. @@ -80,32 +84,30 @@ impl FuncArgs { where T: Cast<Spanned<Value>>, { - (0 .. self.items.len()).find_map(|index| { - let slot = self.items.get_mut(index)?; + for (i, slot) in self.items.iter().enumerate() { if slot.name.is_none() { if T::is(&slot.value) { - let value = self.items.remove(index).value; + let value = self.items.remove(i).value; 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. - pub fn expect<T>(&mut self, ctx: &mut EvalContext, what: &str) -> Option<T> + pub fn expect<T>(&mut self, what: &str) -> TypResult<T> where T: Cast<Spanned<Value>>, { - let found = self.eat(); - if found.is_none() { - ctx.diag(error!(self.span, "missing argument: {}", what)); + match self.eat() { + Some(found) => Ok(found), + 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> + '_ where T: Cast<Spanned<Value>>, @@ -113,35 +115,34 @@ impl FuncArgs { 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. - 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 T: Cast<Spanned<Value>>, { - let index = self + let index = match self .items .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 span = value.span; - match T::cast(value) { - Ok(t) => Some(t), - Err(msg) => { - ctx.diag(error!(span, "{}", msg)); - None - } - } + T::cast(value).map(Some).map_err(Error::partial(self.file, span)) } - /// Produce "unexpected argument" errors for all remaining arguments. - pub fn finish(self, ctx: &mut EvalContext) { - for arg in &self.items { - if arg.value.v != Value::Error { - ctx.diag(error!(arg.span, "unexpected argument")); - } + /// Return an "unexpected argument" error if there is any remaining + /// argument. + pub fn finish(self) -> TypResult<()> { + if let Some(arg) = self.items.first() { + bail!(self.file, arg.span, "unexpected argument"); } + Ok(()) } } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 682a3855..decd4281 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -25,22 +25,21 @@ use std::mem; use std::path::Path; use std::rc::Rc; -use crate::diag::{Diag, DiagSet, Pass}; -use crate::util::EcoString; +use crate::diag::{Error, StrResult, TypResult}; use crate::geom::{Angle, Fractional, Length, Relative}; use crate::image::ImageCache; use crate::loading::{FileId, Loader}; use crate::parse::parse; use crate::syntax::visit::Visit; use crate::syntax::*; +use crate::util::EcoString; use crate::Context; /// 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 template = ast.eval(&mut ctx); - let module = Module { scope: ctx.scopes.top, template }; - Pass::new(module, ctx.diags) + let template = ast.eval(&mut ctx)?; + Ok(Module { scope: ctx.scopes.top, template }) } /// Caches evaluated modules. @@ -61,7 +60,7 @@ pub trait Eval { type Output; /// 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. @@ -74,10 +73,12 @@ pub struct EvalContext<'a> { pub modules: &'a mut ModuleCache, /// The active scopes. pub scopes: Scopes<'a>, - /// Evaluation diagnostics. - pub diags: DiagSet, + /// The currently evaluated file. + pub file: FileId, /// The stack of imported files that led to evaluation of the current file. pub route: Vec<FileId>, + /// The expression map for the currently built template. + pub map: ExprMap, } impl<'a> EvalContext<'a> { @@ -88,141 +89,123 @@ impl<'a> EvalContext<'a> { images: &mut ctx.images, modules: &mut ctx.modules, scopes: Scopes::new(Some(&ctx.std)), - diags: DiagSet::new(), - route: vec![file], + file, + route: vec![], + map: ExprMap::new(), } } /// Resolve a path relative to the current file. /// - /// Generates an error if the file is not found. - pub fn resolve(&mut self, path: &str, span: Span) -> Option<FileId> { - let base = *self.route.last()?; - self.loader.resolve_from(base, Path::new(path)).ok().or_else(|| { - self.diag(error!(span, "file not found")); - None - }) + /// Returns an error if the file is not found. + pub fn resolve(&mut self, path: &str, span: Span) -> TypResult<FileId> { + self.loader + .resolve_from(self.file, Path::new(path)) + .map_err(|_| Error::boxed(self.file, span, "file not found")) } /// 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)?; // Prevent cyclic importing. - if self.route.contains(&id) { - self.diag(error!(span, "cyclic import")); - return None; + if self.file == id || self.route.contains(&id) { + bail!(self.file, span, "cyclic import"); } // Check whether the module was already loaded. if self.modules.get(&id).is_some() { - return Some(id); + return Ok(id); } - let buffer = self.loader.load_file(id).ok().or_else(|| { - self.diag(error!(span, "failed to load file")); - None - })?; + // Load the source file. + let buffer = self + .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(|| { - self.diag(error!(span, "file is not valid utf-8")); - None - })?; + // Decode UTF-8. + let string = std::str::from_utf8(&buffer) + .map_err(|_| Error::boxed(self.file, span, "file is not valid utf-8"))?; // Parse the file. - let parsed = parse(string); + let ast = parse(id, string)?; // Prepare the new context. let new_scopes = Scopes::new(self.scopes.base); let old_scopes = mem::replace(&mut self.scopes, new_scopes); - let old_diags = mem::replace(&mut self.diags, parsed.diags); - self.route.push(id); + self.route.push(self.file); + self.file = id; // Evaluate the module. - let ast = Rc::new(parsed.output); - let template = ast.eval(self); + let template = Rc::new(ast).eval(self)?; // Restore the old context. let new_scopes = mem::replace(&mut self.scopes, old_scopes); - let new_diags = mem::replace(&mut self.diags, old_diags); - self.route.pop(); - - // Put all diagnostics from the module on the import. - for mut diag in new_diags { - diag.span = span; - self.diag(diag); - } + self.file = self.route.pop().unwrap(); // Save the evaluated module. let module = Module { scope: new_scopes.top, template }; self.modules.insert(id, module); - Some(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 - } - } + Ok(id) } } impl Eval for Rc<SyntaxTree> { type Output = Template; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - struct ExprVisitor<'a, 'b> { - ctx: &'a mut EvalContext<'b>, - map: ExprMap, + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + trait Walk { + fn walk(&self, ctx: &mut EvalContext) -> TypResult<()>; } - impl<'ast> Visit<'ast> for ExprVisitor<'_, '_> { - fn visit_expr(&mut self, node: &'ast Expr) { - self.map.insert(node as *const _, node.eval(self.ctx)); + impl Walk for SyntaxTree { + fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { + for node in self.iter() { + node.walk(ctx)?; + } + Ok(()) } } - let mut visitor = ExprVisitor { ctx, map: ExprMap::new() }; - visitor.visit_tree(self); + impl Walk for SyntaxNode { + 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(()) + } + } + + let map = { + let prev = mem::take(&mut ctx.map); + self.walk(ctx)?; + mem::replace(&mut ctx.map, prev) + }; - TemplateTree { tree: Rc::clone(self), map: visitor.map }.into() + Ok(TemplateTree { tree: Rc::clone(self), map }.into()) } } impl Eval for Expr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - match *self { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + Ok(match *self { Self::None(_) => Value::None, Self::Auto(_) => Value::Auto, Self::Bool(_, v) => Value::Bool(v), @@ -235,35 +218,32 @@ impl Eval for Expr { Self::Str(_, ref v) => Value::Str(v.clone()), Self::Ident(ref v) => match ctx.scopes.get(&v) { Some(slot) => slot.borrow().clone(), - None => { - ctx.diag(error!(v.span, "unknown variable")); - Value::Error - } + None => bail!(ctx.file, v.span, "unknown variable"), }, - Self::Array(ref v) => Value::Array(v.eval(ctx)), - Self::Dict(ref v) => Value::Dict(v.eval(ctx)), - Self::Template(ref v) => Value::Template(v.eval(ctx)), - Self::Group(ref v) => v.eval(ctx), - Self::Block(ref v) => v.eval(ctx), - Self::Call(ref v) => v.eval(ctx), - Self::Closure(ref v) => v.eval(ctx), - Self::With(ref v) => v.eval(ctx), - Self::Unary(ref v) => v.eval(ctx), - Self::Binary(ref v) => v.eval(ctx), - Self::Let(ref v) => v.eval(ctx), - Self::If(ref v) => v.eval(ctx), - Self::While(ref v) => v.eval(ctx), - Self::For(ref v) => v.eval(ctx), - Self::Import(ref v) => v.eval(ctx), - Self::Include(ref v) => v.eval(ctx), - } + Self::Array(ref v) => Value::Array(v.eval(ctx)?), + Self::Dict(ref v) => Value::Dict(v.eval(ctx)?), + Self::Template(ref v) => Value::Template(v.eval(ctx)?), + Self::Group(ref v) => v.eval(ctx)?, + Self::Block(ref v) => v.eval(ctx)?, + Self::Call(ref v) => v.eval(ctx)?, + Self::Closure(ref v) => v.eval(ctx)?, + Self::With(ref v) => v.eval(ctx)?, + Self::Unary(ref v) => v.eval(ctx)?, + Self::Binary(ref v) => v.eval(ctx)?, + Self::Let(ref v) => v.eval(ctx)?, + Self::If(ref v) => v.eval(ctx)?, + Self::While(ref v) => v.eval(ctx)?, + Self::For(ref v) => v.eval(ctx)?, + Self::Import(ref v) => v.eval(ctx)?, + Self::Include(ref v) => v.eval(ctx)?, + }) } } impl Eval for ArrayExpr { 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() } } @@ -271,10 +251,10 @@ impl Eval for ArrayExpr { impl Eval for DictExpr { type Output = Dict; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { self.items .iter() - .map(|Named { name, expr }| (name.string.clone(), expr.eval(ctx))) + .map(|Named { name, expr }| Ok((name.string.clone(), expr.eval(ctx)?))) .collect() } } @@ -282,7 +262,7 @@ impl Eval for DictExpr { impl Eval for TemplateExpr { type Output = Template; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { self.tree.eval(ctx) } } @@ -290,7 +270,7 @@ impl Eval for TemplateExpr { impl Eval for GroupExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { self.expr.eval(ctx) } } @@ -298,58 +278,44 @@ impl Eval for GroupExpr { impl Eval for BlockExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { if self.scoping { ctx.scopes.enter(); } let mut output = Value::None; for expr in &self.exprs { - let value = expr.eval(ctx); - output = ctx.join(output, value, expr.span()); + let value = expr.eval(ctx)?; + output = ops::join(output, value) + .map_err(Error::partial(ctx.file, expr.span()))?; } if self.scoping { ctx.scopes.exit(); } - output + Ok(output) } } impl Eval for UnaryExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let value = self.expr.eval(ctx); - if value == Value::Error { - return Value::Error; - } - - let ty = value.type_name(); - let out = match self.op { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let value = self.expr.eval(ctx)?; + let result = match self.op { UnOp::Pos => ops::pos(value), UnOp::Neg => ops::neg(value), UnOp::Not => ops::not(value), }; - - if out == Value::Error { - ctx.diag(error!( - self.span, - "cannot apply '{}' to {}", - self.op.as_str(), - ty, - )); - } - - out + result.map_err(Error::partial(ctx.file, self.span)) } } impl Eval for BinaryExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { match self.op { BinOp::Add => self.apply(ctx, ops::add), BinOp::Sub => self.apply(ctx, ops::sub), @@ -363,7 +329,7 @@ impl Eval for BinaryExpr { BinOp::Leq => self.apply(ctx, ops::leq), BinOp::Gt => self.apply(ctx, ops::gt), 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::SubAssign => self.assign(ctx, ops::sub), BinOp::MulAssign => self.assign(ctx, ops::mul), @@ -375,131 +341,110 @@ impl Eval for BinaryExpr { impl BinaryExpr { /// 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 - F: FnOnce(Value, Value) -> Value, + F: FnOnce(Value, Value) -> StrResult<Value>, { - // Short-circuit boolean operations. - let lhs = self.lhs.eval(ctx); - match (self.op, &lhs) { - (BinOp::And, Value::Bool(false)) => return lhs, - (BinOp::Or, Value::Bool(true)) => return lhs, - _ => {} - } + let lhs = self.lhs.eval(ctx)?; - let rhs = self.rhs.eval(ctx); - if lhs == Value::Error || rhs == Value::Error { - 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); + // Short-circuit boolean operations. + if (self.op == BinOp::And && lhs == Value::Bool(false)) + || (self.op == BinOp::Or && lhs == Value::Bool(true)) + { + return Ok(lhs); } - out + let rhs = self.rhs.eval(ctx)?; + op(lhs, rhs).map_err(Error::partial(ctx.file, self.span)) } /// 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 - 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() { match ctx.scopes.get(id) { Some(slot) => Rc::clone(slot), - None => { - ctx.diag(error!(self.lhs.span(), "unknown variable")); - return Value::Error; - } + None => bail!(ctx.file, lspan, "unknown variable"), } } else { - ctx.diag(error!(self.lhs.span(), "cannot assign to this expression")); - return Value::Error; + bail!(ctx.file, lspan, "cannot assign to this expression",); }; - let rhs = self.rhs.eval(ctx); + let rhs = self.rhs.eval(ctx)?; let mut mutable = match slot.try_borrow_mut() { Ok(mutable) => mutable, Err(_) => { - ctx.diag(error!(self.lhs.span(), "cannot assign to a constant")); - return Value::Error; + bail!(ctx.file, lspan, "cannot assign to a constant",); } }; let lhs = mem::take(&mut *mutable); - let types = (lhs.type_name(), rhs.type_name()); - *mutable = op(lhs, rhs); + *mutable = op(lhs, rhs).map_err(Error::partial(ctx.file, self.span))?; - if *mutable == Value::Error { - 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), - })); + Ok(Value::None) } } impl Eval for CallExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let callee = self.callee.eval(ctx); - if let Some(func) = ctx.cast::<Function>(callee, self.callee.span()) { - let mut args = self.args.eval(ctx); - let returned = func(ctx, &mut args); - args.finish(ctx); - returned - } else { - Value::Error - } + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let callee = self + .callee + .eval(ctx)? + .cast::<Function>() + .map_err(Error::partial(ctx.file, self.callee.span()))?; + + let mut args = self.args.eval(ctx)?; + let returned = callee(ctx, &mut args)?; + args.finish()?; + + Ok(returned) } } impl Eval for CallArgs { type Output = FuncArgs; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let items = self.items.iter().map(|arg| arg.eval(ctx)).collect(); - FuncArgs { span: self.span, items } + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + Ok(FuncArgs { + file: ctx.file, + span: self.span, + items: self + .items + .iter() + .map(|arg| arg.eval(ctx)) + .collect::<TypResult<Vec<_>>>()?, + }) } } impl Eval for CallArg { type Output = FuncArg; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - match self { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + Ok(match self { Self::Pos(expr) => FuncArg { span: self.span(), name: None, - value: Spanned::new(expr.eval(ctx), expr.span()), + value: Spanned::new(expr.eval(ctx)?, expr.span()), }, Self::Named(Named { name, expr }) => FuncArg { span: self.span(), 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 { 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 body = Rc::clone(&self.body); @@ -511,86 +456,92 @@ impl Eval for ClosureExpr { }; 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 // scope of captured variables we collected earlier. let prev = mem::take(&mut ctx.scopes); ctx.scopes.top = captured.clone(); for param in params.iter() { - // Set the parameter to `none` if the argument is missing. - let value = args.expect::<Value>(ctx, param.as_str()).unwrap_or_default(); + let value = args.expect::<Value>(param.as_str())?; ctx.scopes.def_mut(param.as_str(), value); } - let value = body.eval(ctx); + let result = body.eval(ctx); ctx.scopes = prev; - value - })) + result + }); + + Ok(Value::Func(func)) } } impl Eval for WithExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let callee = self.callee.eval(ctx); - if let Some(func) = ctx.cast::<Function>(callee, self.callee.span()) { - let applied = self.args.eval(ctx); - let name = func.name().cloned(); - Value::Func(Function::new(name, move |ctx, args| { - // 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 - // are in the right order. - args.items.splice(.. 0, kept); - - // Call the original function. - func(ctx, args) - })) - } else { - Value::Error - } + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let callee = self + .callee + .eval(ctx)? + .cast::<Function>() + .map_err(Error::partial(ctx.file, self.callee.span()))?; + + let applied = self.args.eval(ctx)?; + + let name = callee.name().cloned(); + let func = Function::new(name, move |ctx, args| { + // 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 + // 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 { 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 { - Some(expr) => expr.eval(ctx), + Some(expr) => expr.eval(ctx)?, None => Value::None, }; ctx.scopes.def_mut(self.binding.as_str(), value); - Value::None + Ok(Value::None) } } impl Eval for IfExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let condition = self.condition.eval(ctx); - if let Some(condition) = ctx.cast(condition, self.condition.span()) { - if condition { - self.if_body.eval(ctx) - } else if let Some(else_body) = &self.else_body { - else_body.eval(ctx) - } else { - Value::None - } + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let condition = self + .condition + .eval(ctx)? + .cast::<bool>() + .map_err(Error::partial(ctx.file, self.condition.span()))?; + + if condition { + self.if_body.eval(ctx) + } else if let Some(else_body) = &self.else_body { + else_body.eval(ctx) } else { - Value::Error + Ok(Value::None) } } } @@ -598,28 +549,28 @@ impl Eval for IfExpr { impl Eval for WhileExpr { 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; - loop { - let condition = self.condition.eval(ctx); - if let Some(condition) = ctx.cast(condition, self.condition.span()) { - if condition { - let value = self.body.eval(ctx); - output = ctx.join(output, value, self.body.span()); - } else { - return output; - } - } else { - return Value::Error; - } + + while self + .condition + .eval(ctx)? + .cast::<bool>() + .map_err(Error::partial(ctx.file, self.condition.span()))? + { + let value = self.body.eval(ctx)?; + output = ops::join(output, value) + .map_err(Error::partial(ctx.file, self.body.span()))?; } + + Ok(output) } } impl Eval for ForExpr { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { macro_rules! iter { (for ($($binding:ident => $value:ident),*) in $iter:expr) => {{ let mut output = Value::None; @@ -629,17 +580,18 @@ impl Eval for ForExpr { for ($($value),*) in $iter { $(ctx.scopes.def_mut($binding.as_str(), $value);)* - let value = self.body.eval(ctx); - output = ctx.join(output, value, self.body.span()); + let value = self.body.eval(ctx)?; + output = ops::join(output, value) + .map_err(Error::partial(ctx.file, self.body.span()))?; } ctx.scopes.exit(); - output + Ok(output) }}; } - let iter = self.iter.eval(ctx); - match (self.pattern.clone(), iter) { + let iter = self.iter.eval(ctx)?; + match (&self.pattern, iter) { (ForPattern::Value(v), Value::Str(string)) => { 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)) => { iter!(for (k => key, v => value) in dict.into_iter()) } - (ForPattern::KeyValue(_, _), Value::Str(_)) => { - ctx.diag(error!(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 + bail!(ctx.file, self.pattern.span(), "mismatched pattern"); } + (_, 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 { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let path = self.path.eval(ctx); - if let Some(path) = ctx.cast::<EcoString>(path, self.path.span()) { - if let Some(hash) = ctx.import(&path, self.path.span()) { - let mut module = &ctx.modules[&hash]; - match &self.imports { - Imports::Wildcard => { - for (var, slot) in module.scope.iter() { - let value = slot.borrow().clone(); - ctx.scopes.def_mut(var, value); - } - } - Imports::Idents(idents) => { - for ident in idents { - if let Some(slot) = module.scope.get(&ident) { - let value = slot.borrow().clone(); - ctx.scopes.def_mut(ident.as_str(), value); - } else { - ctx.diag(error!(ident.span, "unresolved import")); - module = &ctx.modules[&hash]; - } - } + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let path = self + .path + .eval(ctx)? + .cast::<EcoString>() + .map_err(Error::partial(ctx.file, self.path.span()))?; + + let id = ctx.import(&path, self.path.span())?; + let module = &ctx.modules[&id]; + + match &self.imports { + Imports::Wildcard => { + for (var, slot) in module.scope.iter() { + ctx.scopes.def_mut(var, slot.borrow().clone()); + } + } + Imports::Idents(idents) => { + for ident in idents { + if let Some(slot) = module.scope.get(&ident) { + 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 { type Output = Value; - fn eval(&self, ctx: &mut EvalContext) -> Self::Output { - let path = self.path.eval(ctx); - if let Some(path) = ctx.cast::<EcoString>(path, self.path.span()) { - if let Some(hash) = ctx.import(&path, self.path.span()) { - return Value::Template(ctx.modules[&hash].template.clone()); - } - } + fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> { + let path = self + .path + .eval(ctx)? + .cast::<EcoString>() + .map_err(Error::partial(ctx.file, self.path.span()))?; + + let id = ctx.import(&path, self.path.span())?; + let module = &ctx.modules[&id]; - Value::Error + Ok(Value::Template(module.template.clone())) } } diff --git a/src/eval/ops.rs b/src/eval/ops.rs index df8babe2..2bf1c189 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -1,30 +1,34 @@ use std::cmp::Ordering; use super::Value; +use crate::diag::StrResult; 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. -pub fn join(lhs: Value, rhs: Value) -> Result<Value, Value> { +pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { Ok(match (lhs, rhs) { - (_, Error) => Error, - (Error, _) => Error, (a, None) => a, (None, b) => b, - (Str(a), Str(b)) => Str(a + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), (Template(a), Template(b)) => Template(a + b), (Template(a), Str(b)) => Template(a + b), (Str(a), Template(b)) => Template(a + b), - - (lhs, _) => return Err(lhs), + (a, b) => mismatch!("cannot join {} with {}", a, b), }) } /// Apply the plus operator to a value. -pub fn pos(value: Value) -> Value { - match value { +pub fn pos(value: Value) -> StrResult<Value> { + Ok(match value { Int(v) => Int(v), Float(v) => Float(v), Length(v) => Length(v), @@ -32,13 +36,13 @@ pub fn pos(value: Value) -> Value { Relative(v) => Relative(v), Linear(v) => Linear(v), Fractional(v) => Fractional(v), - _ => Error, - } + v => mismatch!("cannot apply '+' to {}", v), + }) } /// Compute the negation of a value. -pub fn neg(value: Value) -> Value { - match value { +pub fn neg(value: Value) -> StrResult<Value> { + Ok(match value { Int(v) => Int(-v), Float(v) => Float(-v), Length(v) => Length(-v), @@ -46,13 +50,13 @@ pub fn neg(value: Value) -> Value { Relative(v) => Relative(-v), Linear(v) => Linear(-v), Fractional(v) => Fractional(-v), - _ => Error, - } + v => mismatch!("cannot apply '-' to {}", v), + }) } /// Compute the sum of two values. -pub fn add(lhs: Value, rhs: Value) -> Value { - match (lhs, rhs) { +pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { (Int(a), Int(b)) => Int(a + b), (Int(a), Float(b)) => Float(a as f64 + b), (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), (Str(a), Template(b)) => Template(a + b), - _ => Error, - } + (a, b) => mismatch!("cannot add {} and {}", a, b), + }) } /// Compute the difference of two values. -pub fn sub(lhs: Value, rhs: Value) -> Value { - match (lhs, rhs) { +pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { (Int(a), Int(b)) => Int(a - b), (Int(a), Float(b)) => Float(a as f64 - b), (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), - _ => Error, - } + (a, b) => mismatch!("cannot subtract {1} from {0}", a, b), + }) } /// Compute the product of two values. -pub fn mul(lhs: Value, rhs: Value) -> Value { - match (lhs, rhs) { +pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { (Int(a), Int(b)) => Int(a * b), (Int(a), Float(b)) => Float(a as f64 * b), (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)), (Array(a), Int(b)) => Array(a.repeat(b.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. -pub fn div(lhs: Value, rhs: Value) -> Value { - match (lhs, rhs) { +pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { (Int(a), Int(b)) => Float(a as f64 / b as f64), (Int(a), Float(b)) => Float(a as f64 / b), (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), Float(b)) => Linear(a / b), - _ => Error, - } + (a, b) => mismatch!("cannot divide {} by {}", a, b), + }) } /// Compute the logical "not" of a value. -pub fn not(value: Value) -> Value { +pub fn not(value: Value) -> StrResult<Value> { match value { - Bool(b) => Bool(!b), - _ => Error, + Bool(b) => Ok(Bool(!b)), + v => mismatch!("cannot apply 'not' to {}", v), } } /// 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) { - (Bool(a), Bool(b)) => Bool(a && b), - _ => Error, + (Bool(a), Bool(b)) => Ok(Bool(a && b)), + (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b), } } /// 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) { - (Bool(a), Bool(b)) => Bool(a || b), - _ => Error, + (Bool(a), Bool(b)) => Ok(Bool(a || b)), + (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, (Func(a), Func(b)) => a == b, (Dyn(a), Dyn(b)) => a == b, - (Error, Error) => true, // Some technically different things should compare equal. (&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. pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> { match (lhs, rhs) { @@ -269,26 +300,3 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> { _ => 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, - } -} diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 72b524cf..2eb048fa 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -4,7 +4,9 @@ use std::fmt::{self, Debug, Formatter}; use std::iter; 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. pub type Slot = Rc<RefCell<Value>>; @@ -89,7 +91,7 @@ impl Scope { /// Define a constant function. pub fn def_func<F>(&mut self, name: impl Into<EcoString>, f: F) where - F: Fn(&mut EvalContext, &mut FuncArgs) -> Value + 'static, + F: Fn(&mut EvalContext, &mut FuncArgs) -> TypResult<Value> + 'static, { let name = name.into(); self.def_const(name.clone(), Function::new(Some(name), f)); diff --git a/src/eval/template.rs b/src/eval/template.rs index 9a71ada4..29b5663d 100644 --- a/src/eval/template.rs +++ b/src/eval/template.rs @@ -21,9 +21,16 @@ impl Template { } /// 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() } + + /// 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 { diff --git a/src/eval/value.rs b/src/eval/value.rs index b7fdcbc2..9bab067c 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -5,10 +5,11 @@ use std::rc::Rc; use super::{ops, Array, Dict, Function, Template, TemplateFunc}; use crate::color::{Color, RgbaColor}; -use crate::util::EcoString; +use crate::diag::StrResult; use crate::exec::ExecContext; use crate::geom::{Angle, Fractional, Length, Linear, Relative}; use crate::syntax::Spanned; +use crate::util::EcoString; /// A computational value. #[derive(Debug, Clone)] @@ -47,8 +48,6 @@ pub enum Value { Func(Function), /// A dynamic value. Dyn(Dynamic), - /// The result of invalid operations. - Error, } impl Value { @@ -80,7 +79,6 @@ impl Value { Self::Template(_) => Template::TYPE_NAME, Self::Func(_) => Function::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. - pub fn cast<T>(self) -> Result<T, String> + pub fn cast<T>(self) -> StrResult<T> where T: Cast<Value>, { @@ -241,7 +239,7 @@ pub trait Cast<V>: Sized { fn is(value: &V) -> bool; /// 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 { @@ -249,7 +247,7 @@ impl Cast<Value> for Value { true } - fn cast(value: Value) -> Result<Self, String> { + fn cast(value: Value) -> StrResult<Self> { Ok(value) } } @@ -262,7 +260,7 @@ where T::is(&value.v) } - fn cast(value: Spanned<Value>) -> Result<Self, String> { + fn cast(value: Spanned<Value>) -> StrResult<Self> { T::cast(value.v) } } @@ -275,7 +273,7 @@ where T::is(&value.v) } - fn cast(value: Spanned<Value>) -> Result<Self, String> { + fn cast(value: Spanned<Value>) -> StrResult<Self> { let span = value.span; T::cast(value.v).map(|t| Spanned::new(t, span)) } @@ -302,7 +300,7 @@ macro_rules! primitive { matches!(value, Value::$variant(_) $(| Value::$other(_))*) } - fn cast(value: Value) -> Result<Self, String> { + fn cast(value: Value) -> StrResult<Self> { match value { Value::$variant(v) => Ok(v), $(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 { $($pattern => return Ok($out),)* $crate::eval::Value::Dyn(dynamic) => { @@ -387,6 +385,6 @@ primitive! { Color: "color", Color } primitive! { EcoString: "string", Str } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } -primitive! { Template: "template", Template, Str(v) => v.into() } +primitive! { Template: "template", Template } primitive! { Function: "function", Func } primitive! { f64: "float", Float, Int(v) => v as f64 } diff --git a/src/exec/context.rs b/src/exec/context.rs index 4d351692..2d419a56 100644 --- a/src/exec/context.rs +++ b/src/exec/context.rs @@ -2,13 +2,12 @@ use std::mem; use std::rc::Rc; use super::{Exec, ExecWithMap, State}; -use crate::diag::{Diag, DiagSet, Pass}; use crate::eval::{ExprMap, Template}; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ LayoutNode, LayoutTree, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, }; -use crate::syntax::{Span, SyntaxTree}; +use crate::syntax::SyntaxTree; use crate::util::EcoString; use crate::Context; @@ -16,8 +15,6 @@ use crate::Context; pub struct ExecContext { /// The active execution state. pub state: State, - /// Execution diagnostics. - pub diags: DiagSet, /// The tree of finished page runs. tree: LayoutTree, /// 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 { Self { state: ctx.state.clone(), - diags: DiagSet::new(), tree: LayoutTree { runs: vec![] }, page: Some(PageBuilder::new(&ctx.state, true)), 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. pub fn exec_template_stack(&mut self, template: &Template) -> StackNode { self.exec_stack(|ctx| template.exec(ctx)) @@ -127,21 +118,19 @@ impl ExecContext { } /// 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 { let page = mem::replace(builder, PageBuilder::new(&self.state, hard)); let stack = mem::replace(&mut self.stack, StackBuilder::new(&self.state)); 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. - pub fn finish(mut self) -> Pass<LayoutTree> { + pub fn finish(mut self) -> LayoutTree { assert!(self.page.is_some()); - self.pagebreak(true, false, Span::default()); - Pass::new(self.tree, self.diags) + self.pagebreak(true, false); + self.tree } fn make_text_node(&self, text: impl Into<EcoString>) -> ParChild { diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 8bac76e8..762b555d 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -8,7 +8,6 @@ pub use state::*; use std::fmt::Write; -use crate::diag::Pass; use crate::eval::{ExprMap, Template, TemplateFunc, TemplateNode, TemplateTree, Value}; use crate::geom::Gen; use crate::layout::{LayoutTree, StackChild, StackNode}; @@ -18,7 +17,7 @@ use crate::util::EcoString; use crate::Context; /// 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); template.exec(&mut ctx); ctx.finish() @@ -138,7 +137,6 @@ impl Exec for Value { Value::Float(v) => ctx.push_text(pretty(v)), Value::Str(v) => ctx.push_text(v), Value::Template(v) => v.exec(ctx), - Value::Error => {} // For values which can't be shown "naturally", we print the // representation in monospace. other => ctx.push_monospace_text(pretty(other)), diff --git a/src/font.rs b/src/font.rs index 3dec3a63..94548699 100644 --- a/src/font.rs +++ b/src/font.rs @@ -316,7 +316,8 @@ impl FontCache { } /// 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); impl FaceId { diff --git a/src/image.rs b/src/image.rs index 5738be5f..93b95bda 100644 --- a/src/image.rs +++ b/src/image.rs @@ -103,7 +103,8 @@ impl ImageCache { } /// 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); impl ImageId { @@ -7,9 +7,9 @@ //! module. //! - **Evaluation:** The next step is to [evaluate] the syntax tree. This //! computes the value of each node in the document and produces a [module]. -//! - **Execution:** Now, we can [execute] the parsed and evaluated module. -//! This produces a [layout tree], a high-level, fully styled representation -//! of the document. The nodes of this tree are self-contained and +//! - **Execution:** Now, we can [execute] the parsed and evaluated module. This +//! results in a [layout tree], a high-level, fully styled representation of +//! the document. The nodes of this tree are self-contained and //! order-independent and thus much better suited for layouting than the //! syntax tree. //! - **Layouting:** Next, the tree is [layouted] into a portable version of the @@ -49,7 +49,7 @@ pub mod util; use std::rc::Rc; -use crate::diag::Pass; +use crate::diag::TypResult; use crate::eval::{ModuleCache, Scope}; use crate::exec::State; use crate::font::FontCache; @@ -100,19 +100,15 @@ impl Context { /// The `file` identifies the source file and is used to resolve relative /// paths (for importing and image loading). /// - /// Returns a vector of frames representing individual pages alongside - /// diagnostic information (errors and warnings). - pub fn typeset(&mut self, file: FileId, src: &str) -> Pass<Vec<Rc<Frame>>> { - let ast = parse::parse(src); - let module = eval::eval(self, file, Rc::new(ast.output)); - let tree = exec::exec(self, &module.output.template); - let frames = layout::layout(self, &tree.output); - - let mut diags = ast.diags; - diags.extend(module.diags); - diags.extend(tree.diags); - - Pass::new(frames, diags) + /// Returns either a vector of frames representing individual pages or + /// diagnostics in the form of a vector of error message with file and span + /// information. + pub fn typeset(&mut self, file: FileId, src: &str) -> TypResult<Vec<Rc<Frame>>> { + let ast = parse::parse(file, src)?; + let module = eval::eval(self, file, Rc::new(ast))?; + let tree = exec::exec(self, &module.template); + let frames = layout::layout(self, &tree); + Ok(frames) } } diff --git a/src/library/elements.rs b/src/library/elements.rs index afd7444e..3d318d36 100644 --- a/src/library/elements.rs +++ b/src/library/elements.rs @@ -8,46 +8,44 @@ use crate::layout::{ }; /// `image`: An image. -pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let path = args.expect::<Spanned<EcoString>>(ctx, "path to image file"); - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); - - let mut node = None; - if let Some(path) = &path { - if let Some(file) = ctx.resolve(&path.v, path.span) { - if let Some(id) = ctx.images.load(file) { - node = Some(ImageNode { id, width, height }); - } else { - ctx.diag(error!(path.span, "failed to load image")); - } - } - } - - Value::template(move |ctx| { - if let Some(node) = node { - ctx.push_into_par(node); - } - }) +pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let path = args.expect::<Spanned<EcoString>>("path to image file")?; + let width = args.named("width")?; + let height = args.named("height")?; + + let file = ctx.resolve(&path.v, path.span)?; + let node = match ctx.images.load(file) { + Some(id) => ImageNode { id, width, height }, + None => bail!(args.file, path.span, "failed to load image"), + }; + + Ok(Value::template(move |ctx| ctx.push_into_par(node))) } /// `rect`: A rectangle with optional content. -pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); - let fill = args.named(ctx, "fill"); +pub fn rect(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let width = args.named("width")?; + let height = args.named("height")?; + let fill = args.named("fill")?; 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. -pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let length = args.named::<Length>(ctx, "length").map(Linear::from); - let width = length.or_else(|| args.named(ctx, "width")); - let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); - let fill = args.named(ctx, "fill"); +pub fn square(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let length = args.named::<Length>("length")?.map(Linear::from); + let width = match length { + Some(length) => Some(length), + 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(); - rect_impl(width, height, Some(N64::from(1.0)), fill, body) + Ok(rect_impl(width, height, aspect, fill, body)) } fn rect_impl( @@ -76,22 +74,29 @@ fn rect_impl( } /// `ellipse`: An ellipse with optional content. -pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); - let fill = args.named(ctx, "fill"); +pub fn ellipse(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let width = args.named("width")?; + let height = args.named("height")?; + let fill = args.named("fill")?; 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. -pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let diameter = args.named::<Length>(ctx, "radius").map(|r| 2.0 * Linear::from(r)); - let width = diameter.or_else(|| args.named(ctx, "width")); - let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); - let fill = args.named(ctx, "fill"); +pub fn circle(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let diameter = args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r)); + let width = match diameter { + None => args.named("width")?, + 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(); - ellipse_impl(width, height, Some(N64::from(1.0)), fill, body) + Ok(ellipse_impl(width, height, aspect, fill, body)) } fn ellipse_impl( diff --git a/src/library/layout.rs b/src/library/layout.rs index f3f3f81e..727bbcc3 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -3,26 +3,26 @@ use crate::layout::{FixedNode, GridNode, PadNode, StackChild, StackNode, TrackSi use crate::paper::{Paper, PaperClass}; /// `page`: Configure pages. -pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let span = args.span; - let paper = args.eat::<Spanned<EcoString>>().and_then(|name| { - Paper::from_name(&name.v).or_else(|| { - ctx.diag(error!(name.span, "invalid paper name")); - None - }) - }); - - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); - let margins = args.named(ctx, "margins"); - let left = args.named(ctx, "left"); - let top = args.named(ctx, "top"); - let right = args.named(ctx, "right"); - let bottom = args.named(ctx, "bottom"); - let flip = args.named(ctx, "flip"); - let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); - - Value::template(move |ctx| { +pub fn page(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let paper = match args.eat::<Spanned<EcoString>>() { + Some(name) => match Paper::from_name(&name.v) { + None => bail!(args.file, name.span, "invalid paper name"), + paper => paper, + }, + None => None, + }; + + let width = args.named("width")?; + let height = args.named("height")?; + let margins = args.named("margins")?; + let left = args.named("left")?; + let top = args.named("top")?; + let right = args.named("right")?; + let bottom = args.named("bottom")?; + let flip = args.named("flip")?; + let body = args.expect::<Template>("body")?; + + Ok(Value::template(move |ctx| { let snapshot = ctx.state.clone(); 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); } - ctx.pagebreak(false, true, span); + ctx.pagebreak(false, true); body.exec(ctx); ctx.state = snapshot; - ctx.pagebreak(true, false, span); - }) + ctx.pagebreak(true, false); + })) } /// `pagebreak`: Start a new page. -pub fn pagebreak(_: &mut EvalContext, args: &mut FuncArgs) -> Value { - let span = args.span; - Value::template(move |ctx| { - ctx.pagebreak(true, true, span); - }) +pub fn pagebreak(_: &mut EvalContext, _: &mut FuncArgs) -> TypResult<Value> { + Ok(Value::template(move |ctx| ctx.pagebreak(true, true))) } /// `h`: Horizontal spacing. -pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing_impl(ctx, args, GenAxis::Cross) +pub fn h(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + spacing_impl(args, GenAxis::Cross) } /// `v`: Vertical spacing. -pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing_impl(ctx, args, GenAxis::Main) +pub fn v(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + spacing_impl(args, GenAxis::Main) } -fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: GenAxis) -> Value { - let spacing: Option<Linear> = args.expect(ctx, "spacing"); - Value::template(move |ctx| { - if let Some(linear) = spacing { - // TODO: Should this really always be font-size relative? - let amount = linear.resolve(ctx.state.font.size); - ctx.push_spacing(axis, amount); - } - }) +fn spacing_impl(args: &mut FuncArgs, axis: GenAxis) -> TypResult<Value> { + let spacing = args.expect::<Linear>("spacing")?; + Ok(Value::template(move |ctx| { + // TODO: Should this really always be font-size relative? + let amount = spacing.resolve(ctx.state.font.size); + ctx.push_spacing(axis, amount); + })) } /// `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 second = args.eat::<Align>(); - let mut horizontal = args.named(ctx, "horizontal"); - let mut vertical = args.named(ctx, "vertical"); - let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); + let body = args.expect::<Template>("body")?; for value in first.into_iter().chain(second) { 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 { ctx.state.aligns.cross = horizontal; } @@ -133,37 +128,37 @@ pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } body.exec(ctx); - }) + })) } /// `box`: Place content in a rectangular box. -pub fn boxed(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); +pub fn boxed(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let width = args.named("width")?; + let height = args.named("height")?; let body = args.eat().unwrap_or_default(); - Value::template(move |ctx| { + Ok(Value::template(move |ctx| { let child = ctx.exec_template_stack(&body).into(); ctx.push_into_par(FixedNode { width, height, child }); - }) + })) } /// `block`: Place content in a block. -pub fn block(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let body = args.expect(ctx, "body").unwrap_or_default(); - Value::template(move |ctx| { +pub fn block(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let body = args.expect("body")?; + Ok(Value::template(move |ctx| { let block = ctx.exec_template_stack(&body); ctx.push_into_stack(block); - }) + })) } /// `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 left = args.named(ctx, "left"); - let top = args.named(ctx, "top"); - let right = args.named(ctx, "right"); - let bottom = args.named(ctx, "bottom"); - let body = args.expect(ctx, "body").unwrap_or_default(); + let left = args.named("left")?; + let top = args.named("top")?; + let right = args.named("right")?; + let bottom = args.named("bottom")?; + let body = args.expect("body")?; let padding = Sides::new( 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(), ); - Value::template(move |ctx| { + Ok(Value::template(move |ctx| { let child = ctx.exec_template_stack(&body).into(); ctx.push_into_stack(PadNode { padding, child }); - }) + })) } /// `stack`: Stack children along an axis. -pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let dir = args.named(ctx, "dir"); +pub fn stack(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let dir = args.named("dir")?; let children: Vec<_> = args.all().collect(); - Value::template(move |ctx| { + Ok(Value::template(move |ctx| { let children = children .iter() .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 }); - }) + })) } /// `grid`: Arrange children into a grid. -pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let columns = args.named(ctx, "columns").unwrap_or_default(); - let rows = args.named(ctx, "rows").unwrap_or_default(); +pub fn grid(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let columns = args.named("columns")?.unwrap_or_default(); + let rows = args.named("rows")?.unwrap_or_default(); - let gutter_columns = args.named(ctx, "gutter-columns"); - let gutter_rows = args.named(ctx, "gutter-rows"); + let gutter_columns = args.named("gutter-columns")?; + let gutter_rows = args.named("gutter-rows")?; let default = args - .named(ctx, "gutter") + .named("gutter")? .map(|v| vec![TrackSizing::Linear(v)]) .unwrap_or_default(); - let column_dir = args.named(ctx, "column-dir"); - let row_dir = args.named(ctx, "row-dir"); + let column_dir = args.named("column-dir")?; + let row_dir = args.named("row-dir")?; 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), ); - Value::template(move |ctx| { + Ok(Value::template(move |ctx| { let children = children .iter() .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(), children, }) - }) + })) } /// Defines size of rows and columns in a grid. diff --git a/src/library/mod.rs b/src/library/mod.rs index ff2fb3e8..3591e742 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -17,6 +17,7 @@ use std::convert::TryFrom; use std::rc::Rc; use crate::color::{Color, RgbaColor}; +use crate::diag::TypResult; use crate::eval::{EvalContext, FuncArgs, Scope, Template, Type, Value}; use crate::exec::Exec; use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; diff --git a/src/library/text.rs b/src/library/text.rs index 54e9794a..5973a7e2 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -4,29 +4,27 @@ use crate::layout::Paint; use super::*; /// `font`: Configure the font. -pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let size = args.eat::<Linear>().or_else(|| args.named(ctx, "size")); - let style = args.named(ctx, "style"); - let weight = args.named(ctx, "weight"); - let stretch = args.named(ctx, "stretch"); - let top_edge = args.named(ctx, "top-edge"); - let bottom_edge = args.named(ctx, "bottom-edge"); - let fill = args.named(ctx, "fill"); - - let families: Vec<_> = args.all().collect(); - let list = if families.is_empty() { - args.named(ctx, "family") - } else { - Some(FontDef(Rc::new(families))) - }; +pub fn font(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let size = args.named::<Linear>("size")?.or_else(|| args.eat()); + let style = args.named("style")?; + let weight = args.named("weight")?; + let stretch = args.named("stretch")?; + let top_edge = args.named("top-edge")?; + let bottom_edge = args.named("bottom-edge")?; + let fill = args.named("fill")?; + + let list = args.named("family")?.or_else(|| { + let families: Vec<_> = args.all().collect(); + (!families.is_empty()).then(|| FontDef(Rc::new(families))) + }); - let serif = args.named(ctx, "serif"); - let sans_serif = args.named(ctx, "sans-serif"); - let monospace = args.named(ctx, "monospace"); + let serif = args.named("serif")?; + let sans_serif = args.named("sans-serif")?; + 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(); if let Some(size) = size { @@ -74,7 +72,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } body.exec(ctx); - }) + })) } struct FontDef(Rc<Vec<FontFamily>>); @@ -106,12 +104,12 @@ castable! { } /// `par`: Configure paragraphs. -pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let par_spacing = args.named(ctx, "spacing"); - let line_spacing = args.named(ctx, "leading"); - let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); +pub fn par(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let par_spacing = args.named("spacing")?; + let line_spacing = args.named("leading")?; + let body = args.expect::<Template>("body")?; - Value::template(move |ctx| { + Ok(Value::template(move |ctx| { let state = ctx.state.par_mut(); if let Some(par_spacing) = par_spacing { @@ -124,33 +122,32 @@ pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { ctx.parbreak(); body.exec(ctx); - }) + })) } /// `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 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 { Some(dir.v) } else { - ctx.diag(error!(dir.span, "must be horizontal")); - None + bail!(args.file, dir.span, "must be horizontal"); } } else { 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 { ctx.state.dirs.cross = dir; } ctx.parbreak(); body.exec(ctx); - }) + })) } /// 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. -pub fn strike(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - line_impl(ctx, args, |font| &mut font.strikethrough) +pub fn strike(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + line_impl(args, |font| &mut font.strikethrough) } /// `underline`: Enable underlined text. -pub fn underline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - line_impl(ctx, args, |font| &mut font.underline) +pub fn underline(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + line_impl(args, |font| &mut font.underline) } /// `overline`: Add an overline above text. -pub fn overline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - line_impl(ctx, args, |font| &mut font.overline) +pub fn overline(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + line_impl(args, |font| &mut font.overline) } fn line_impl( - ctx: &mut EvalContext, args: &mut FuncArgs, substate: fn(&mut FontState) -> &mut Option<Rc<LineState>>, -) -> Value { - let stroke = args.eat().or_else(|| args.named(ctx, "stroke")); - let thickness = args.eat::<Linear>().or_else(|| args.named(ctx, "thickness")); - let offset = args.named(ctx, "offset"); - let extent = args.named(ctx, "extent").unwrap_or_default(); - let body = args.expect::<Template>(ctx, "body").unwrap_or_default(); +) -> TypResult<Value> { + let stroke = args.named("stroke")?.or_else(|| args.eat()); + let thickness = args.named::<Linear>("thickness")?.or_else(|| args.eat()); + let offset = args.named("offset")?; + let extent = args.named("extent")?.unwrap_or_default(); + let body = args.expect::<Template>("body")?; // Suppress any existing strikethrough if strength is explicitly zero. 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(); body.exec(ctx); - }) + })) } diff --git a/src/library/utility.rs b/src/library/utility.rs index 4001d14f..fb39fce3 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -7,94 +7,78 @@ use crate::pretty::pretty; use super::*; /// `type`: The name of a value's type. -pub fn type_(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - match args.expect::<Value>(ctx, "value") { - Some(value) => value.type_name().into(), - None => Value::Error, - } +pub fn type_(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let value = args.expect::<Value>("value")?; + Ok(value.type_name().into()) } /// `repr`: The string representation of a value. -pub fn repr(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - match args.expect::<Value>(ctx, "value") { - Some(value) => pretty(&value).into(), - None => Value::Error, - } +pub fn repr(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let value = args.expect::<Value>("value")?; + Ok(pretty(&value).into()) } /// `len`: The length of a string, an array or a dictionary. -pub fn len(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - match args.expect(ctx, "collection") { - Some(Spanned { v: Value::Str(v), .. }) => Value::Int(v.len() as i64), - Some(Spanned { v: Value::Array(v), .. }) => Value::Int(v.len() as i64), - Some(Spanned { v: Value::Dict(v), .. }) => Value::Int(v.len() as i64), - Some(other) if other.v != Value::Error => { - ctx.diag(error!(other.span, "expected string, array or dictionary")); - Value::Error - } - _ => Value::Error, - } +pub fn len(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + let Spanned { v, span } = args.expect("collection")?; + Ok(match v { + Value::Str(v) => Value::Int(v.len() as i64), + Value::Array(v) => Value::Int(v.len() as i64), + Value::Dict(v) => Value::Int(v.len() as i64), + _ => bail!(args.file, span, "expected string, array or dictionary"), + }) } /// `rgb`: Create an RGB(A) color. -pub fn rgb(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - Value::Color(Color::Rgba( +pub fn rgb(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + Ok(Value::Color(Color::Rgba( if let Some(string) = args.eat::<Spanned<EcoString>>() { match RgbaColor::from_str(&string.v) { Ok(color) => color, - Err(_) => { - ctx.diag(error!(string.span, "invalid color")); - return Value::Error; - } + Err(_) => bail!(args.file, string.span, "invalid color"), } } else { - let r = args.expect(ctx, "red component").unwrap_or(0.0); - let g = args.expect(ctx, "green component").unwrap_or(0.0); - let b = args.expect(ctx, "blue component").unwrap_or(0.0); + let r = args.expect("red component")?; + let g = args.expect("green component")?; + let b = args.expect("blue component")?; let a = args.eat().unwrap_or(1.0); 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)) }, - )) + ))) } /// `min`: The minimum of a sequence of values. -pub fn min(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - minmax(ctx, args, Ordering::Less) +pub fn min(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + minmax(args, Ordering::Less) } /// `max`: The maximum of a sequence of values. -pub fn max(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - minmax(ctx, args, Ordering::Greater) +pub fn max(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> { + minmax(args, Ordering::Greater) } /// Find the minimum or maximum of a sequence of values. -fn minmax(ctx: &mut EvalContext, args: &mut FuncArgs, goal: Ordering) -> Value { - let span = args.span; - let mut extremum = None; +fn minmax(args: &mut FuncArgs, goal: Ordering) -> TypResult<Value> { + let &mut FuncArgs { file, span, .. } = args; + let mut extremum = args.expect::<Value>("value")?; for value in args.all::<Value>() { - if let Some(prev) = &extremum { - match value.partial_cmp(&prev) { - Some(ordering) if ordering == goal => extremum = Some(value), - Some(_) => {} - None => { - ctx.diag(error!( - span, - "cannot compare {} with {}", - prev.type_name(), - value.type_name(), - )); - return Value::Error; + match value.partial_cmp(&extremum) { + Some(ordering) => { + if ordering == goal { + extremum = value; } } - } else { - extremum = Some(value); + None => bail!( + file, + span, + "cannot compare {} with {}", + extremum.type_name(), + value.type_name(), + ), } } - extremum.unwrap_or_else(|| { - args.expect::<Value>(ctx, "value"); - Value::Error - }) + Ok(extremum) } diff --git a/src/loading/mod.rs b/src/loading/mod.rs index 64a65580..ab52439d 100644 --- a/src/loading/mod.rs +++ b/src/loading/mod.rs @@ -34,7 +34,8 @@ pub trait Loader { /// 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. -#[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); impl FileId { diff --git a/src/main.rs b/src/main.rs index 91edff17..7891082d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,32 +35,35 @@ fn main() -> anyhow::Result<()> { .wrap(); // 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) .map_err(|_| anyhow!("failed to read source file"))?; // Typeset. 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. - let map = typst::parse::LineMap::new(&src); - for diag in pass.diags { - let start = map.location(diag.span.start).unwrap(); - let end = map.location(diag.span.end).unwrap(); - println!( - "{}: {}:{}-{}: {}", - diag.level, - src_path.display(), - start, - end, - diag.message, - ); + // Print diagnostics. + Err(errors) => { + let map = typst::parse::LineMap::new(&src); + for error in errors.iter() { + let start = map.location(error.span.start).unwrap(); + let end = map.location(error.span.end).unwrap(); + println!( + "Error: {}:{}-{}: {}", + src_path.display(), + start, + end, + 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(()) } diff --git a/src/parse/lines.rs b/src/parse/lines.rs index e42d110b..2d97a25c 100644 --- a/src/parse/lines.rs +++ b/src/parse/lines.rs @@ -77,8 +77,8 @@ impl<'s> LineMap<'s> { } } -/// Determine the column at the end of the string. -pub fn search_column(src: &str) -> usize { +/// Count how many column the string would fill. +pub fn count_columns(src: &str) -> usize { let mut column = 0; for c in src.chars().rev() { if is_newline(c) { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index a94345a8..70c23442 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -14,14 +14,21 @@ pub use tokens::*; use std::rc::Rc; -use crate::diag::Pass; +use crate::diag::TypResult; +use crate::loading::FileId; use crate::syntax::*; use crate::util::EcoString; /// Parse a string of source code. -pub fn parse(src: &str) -> Pass<SyntaxTree> { - let mut p = Parser::new(src); - Pass::new(tree(&mut p), p.diags) +pub fn parse(file: FileId, src: &str) -> TypResult<SyntaxTree> { + let mut p = Parser::new(file, src); + let tree = tree(&mut p); + let errors = p.finish(); + if errors.is_empty() { + Ok(tree) + } else { + Err(Box::new(errors)) + } } /// 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); let expr = expr_with(p, true, 0); 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(); @@ -160,12 +167,12 @@ fn unicode_escape(p: &mut Parser, token: UnicodeEscapeToken) -> EcoString { c.into() } else { // 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() }; if !token.terminated { - p.diag(error!(span.end, "expected closing brace")); + p.error(span.end, "expected closing brace"); } text @@ -176,7 +183,7 @@ fn raw(p: &mut Parser, token: RawToken) -> SyntaxNode { let span = p.peek_span(); let raw = resolve::resolve_raw(span, token.text, token.backticks); if !token.terminated { - p.diag(error!(p.peek_span().end, "expected backtick(s)")); + p.error(span.end, "expected backtick(s)"); } SyntaxNode::Raw(raw) } @@ -198,11 +205,7 @@ fn heading(p: &mut Parser) -> SyntaxNode { let body = tree_indented(p); - SyntaxNode::Heading(HeadingNode { - span: p.span(start), - level, - body: Rc::new(body), - }) + SyntaxNode::Heading(HeadingNode { span: p.span(start), level, body }) } /// Parse a single list item. @@ -356,7 +359,7 @@ fn literal(p: &mut Parser) -> Option<Expr> { Token::Fraction(p) => Expr::Fractional(span, p), Token::Str(token) => Expr::Str(span, { if !token.terminated { - p.expected_at("quote", p.peek_span().end); + p.expected_at(span.end, "quote"); } resolve::resolve_string(token.string) }), @@ -421,7 +424,7 @@ fn collection(p: &mut Parser) -> (Vec<CallArg>, bool) { items.push(arg); if let Some(pos) = missing_coma.take() { - p.expected_at("comma", pos); + p.expected_at(pos, "comma"); } if p.eof() { @@ -447,7 +450,7 @@ fn item(p: &mut Parser) -> Option<CallArg> { if let Expr::Ident(name) = first { Some(CallArg::Named(Named { name, expr: expr(p)? })) } else { - p.diag(error!(first.span(), "expected identifier")); + p.error(first.span(), "expected identifier"); expr(p); 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 { CallArg::Pos(expr) => Some(expr), CallArg::Named(_) => { - p.diag(error!(item.span(), "expected expression, found named pair")); + p.error(item.span(), "expected expression, found named pair"); 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 { CallArg::Named(named) => Some(named), CallArg::Pos(_) => { - p.diag(error!(item.span(), "expected named pair, found expression")); + p.error(item.span(), "expected named pair, found expression"); 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 { CallArg::Pos(Expr::Ident(id)) => Some(id), _ => { - p.diag(error!(item.span(), "expected identifier")); + p.error(item.span(), "expected identifier"); None } }); @@ -512,7 +515,7 @@ fn block(p: &mut Parser, scoping: bool) -> Expr { if let Some(expr) = expr(p) { exprs.push(expr); 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(); @@ -529,10 +532,7 @@ fn call(p: &mut Parser, callee: Expr) -> Option<Expr> { let mut wide = p.eat_if(Token::Excl); if wide && p.outer_mode() == TokenMode::Code { let span = p.span(callee.span().start); - p.diag(error!( - span, - "wide calls are only allowed directly in templates", - )); + p.error(span, "wide calls are only allowed directly in templates"); wide = false; } @@ -548,7 +548,7 @@ fn call(p: &mut Parser, callee: Expr) -> Option<Expr> { items: vec![], }, _ => { - p.expected_at("argument list", p.prev_end()); + p.expected_at(p.prev_end(), "argument list"); return None; } }; @@ -616,7 +616,7 @@ fn let_expr(p: &mut Parser) -> Option<Expr> { init = expr(p); } else if params.is_some() { // 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. @@ -738,7 +738,7 @@ fn import_expr(p: &mut Parser) -> Option<Expr> { p.start_group(Group::Imports, TokenMode::Code); let items = collection(p).0; if items.is_empty() { - p.expected_at("import items", p.prev_end()); + p.expected_at(p.prev_end(), "import items"); } p.end_group(); Imports::Idents(idents(p, items)) @@ -790,7 +790,7 @@ fn body(p: &mut Parser) -> Option<Expr> { Some(Token::LeftBracket) => Some(template(p)), Some(Token::LeftBrace) => Some(block(p, true)), _ => { - p.expected_at("body", p.prev_end()); + p.expected_at(p.prev_end(), "body"); None } } diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 5fba961a..0238c8be 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -1,14 +1,17 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::Range; -use super::{search_column, TokenMode, Tokens}; -use crate::diag::{Diag, DiagSet}; +use super::{count_columns, TokenMode, Tokens}; +use crate::diag::Error; +use crate::loading::FileId; use crate::syntax::{Pos, Span, Token}; /// A convenient token-based parser. pub struct Parser<'s> { - /// Parsing diagnostics. - pub diags: DiagSet, + /// The id of the parsed file. + file: FileId, + /// Parsing errors. + errors: Vec<Error>, /// An iterator over the source tokens. tokens: Tokens<'s>, /// The stack of open groups. @@ -57,11 +60,12 @@ pub enum Group { impl<'s> Parser<'s> { /// 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 next = tokens.next(); Self { - diags: DiagSet::new(), + file, + errors: vec![], tokens, groups: vec![], next, @@ -71,39 +75,45 @@ impl<'s> Parser<'s> { } } - /// Add a diagnostic. - pub fn diag(&mut self, diag: Diag) { - self.diags.insert(diag); + /// Finish parsing and return all errors. + pub fn finish(self) -> Vec<Error> { + self.errors } - /// Eat the next token and add a diagnostic that it is not the expected - /// `thing`. + /// Add an error with location and message. + 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) { let before = self.next_start(); if let Some(found) = self.eat() { let after = self.prev_end(); - self.diag(error!( + self.error( before .. after, - "expected {}, found {}", - what, - found.name(), - )); + format!("expected {}, found {}", what, found.name()), + ); } 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. - pub fn expected_at(&mut self, what: &str, pos: impl Into<Pos>) { - self.diag(error!(pos.into(), "expected {}", what)); + /// Add an error that `what` was expected at the given position. + pub fn expected_at(&mut self, pos: impl Into<Pos>, what: &str) { + 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) { let before = self.next_start(); if let Some(found) = self.eat() { 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(); rescan = false; } 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()) } - /// Consume the next token if it is the given one and produce a diagnostic - /// if not. + /// Consume the next token if it is the given one and produce an error if + /// not. pub fn expect(&mut self, t: Token) -> bool { let eaten = self.eat_if(t); if !eaten { - self.expected_at(t.name(), self.prev_end()); + self.expected_at(self.prev_end(), t.name()); } eaten } @@ -314,7 +324,7 @@ impl<'s> Parser<'s> { /// Determine the column for the given index in the source. 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). diff --git a/src/pretty.rs b/src/pretty.rs index 10e4fbcb..a4e9b334 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -488,7 +488,6 @@ impl Pretty for Value { Self::Template(v) => v.pretty(p), Self::Func(v) => v.pretty(p), Self::Dyn(v) => v.pretty(p), - Self::Error => p.push_str("<error>"), } } } @@ -609,6 +608,7 @@ pretty_display! { #[cfg(test)] mod tests { use super::*; + use crate::loading::FileId; use crate::parse::parse; #[track_caller] @@ -618,7 +618,7 @@ mod tests { #[track_caller] 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); if exp != found { println!("tree: {:#?}", ast); @@ -732,6 +732,7 @@ mod tests { #[test] fn test_pretty_print_value() { + // Primitives. test_value(Value::None, "none"); test_value(false, "false"); test_value(12i64, "12"); @@ -742,6 +743,8 @@ mod tests { test_value(Relative::new(0.3) + Length::cm(2.0), "30% + 2cm"); test_value(Fractional::one() * 7.55, "7.55fr"); test_value(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101"); + + // Collections. test_value("hello", r#""hello""#); test_value("\n", r#""\n""#); test_value("\\", r#""\\""#); @@ -752,12 +755,15 @@ mod tests { test_value(dict![], "(:)"); test_value(dict!["one" => 1], "(one: 1)"); 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( - Function::new(Some("nil".into()), |_, _| Value::None), + Function::new(Some("nil".into()), |_, _| Ok(Value::None)), "<function nil>", ); + + // Dynamics. test_value(Dynamic::new(1), "1"); - test_value(Value::Error, "<error>"); } } diff --git a/src/syntax/node.rs b/src/syntax/node.rs index 9294fecd..881752a6 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use super::*; /// 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). pub level: usize, /// The contents of the heading. - pub body: Rc<SyntaxTree>, + pub body: SyntaxTree, } /// An item in an unordered list: `- ...`. diff --git a/src/syntax/visit.rs b/src/syntax/visit.rs index b6ee657a..c38e87aa 100644 --- a/src/syntax/visit.rs +++ b/src/syntax/visit.rs @@ -102,7 +102,7 @@ impl_visitors! { } visit_heading(v, heading: HeadingNode) { - v.visit_tree(r!(rc: heading.body)); + v.visit_tree(r!(heading.body)); } visit_list(v, item: ListItem) { diff --git a/src/util/mod.rs b/src/util/mod.rs index 2c53bc70..dc400af8 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -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> { slice: &'a [T], f: F, diff --git a/tests/ref/code/array.png b/tests/ref/code/array.png Binary files differindex b1e20348..c90898c1 100644 --- a/tests/ref/code/array.png +++ b/tests/ref/code/array.png diff --git a/tests/ref/code/block-invalid.png b/tests/ref/code/block-invalid.png Binary files differdeleted file mode 100644 index d1e85402..00000000 --- a/tests/ref/code/block-invalid.png +++ /dev/null diff --git a/tests/ref/code/block.png b/tests/ref/code/block.png Binary files differindex 3e64d82c..afdd176b 100644 --- a/tests/ref/code/block.png +++ b/tests/ref/code/block.png diff --git a/tests/ref/code/call-invalid.png b/tests/ref/code/call-invalid.png Binary files differdeleted file mode 100644 index b2f62ac2..00000000 --- a/tests/ref/code/call-invalid.png +++ /dev/null diff --git a/tests/ref/code/call-wide.png b/tests/ref/code/call-wide.png Binary files differdeleted file mode 100644 index 2d5b4ffe..00000000 --- a/tests/ref/code/call-wide.png +++ /dev/null diff --git a/tests/ref/code/call.png b/tests/ref/code/call.png Binary files differindex fd2abbd8..1839c5d8 100644 --- a/tests/ref/code/call.png +++ b/tests/ref/code/call.png diff --git a/tests/ref/code/closure.png b/tests/ref/code/closure.png Binary files differnew file mode 100644 index 00000000..043d4a11 --- /dev/null +++ b/tests/ref/code/closure.png diff --git a/tests/ref/code/comment.png b/tests/ref/code/comment.png Binary files differindex bb8bf69a..e349b384 100644 --- a/tests/ref/code/comment.png +++ b/tests/ref/code/comment.png diff --git a/tests/ref/code/dict.png b/tests/ref/code/dict.png Binary files differindex 730ff6d8..7b7f4d8c 100644 --- a/tests/ref/code/dict.png +++ b/tests/ref/code/dict.png diff --git a/tests/ref/code/for.png b/tests/ref/code/for.png Binary files differindex de1f3cab..7178b594 100644 --- a/tests/ref/code/for.png +++ b/tests/ref/code/for.png diff --git a/tests/ref/code/if.png b/tests/ref/code/if.png Binary files differindex 48152e23..7b54203a 100644 --- a/tests/ref/code/if.png +++ b/tests/ref/code/if.png diff --git a/tests/ref/code/import.png b/tests/ref/code/import.png Binary files differindex 75d4adce..00595f8a 100644 --- a/tests/ref/code/import.png +++ b/tests/ref/code/import.png diff --git a/tests/ref/code/let.png b/tests/ref/code/let.png Binary files differindex e9c00064..9805b8ee 100644 --- a/tests/ref/code/let.png +++ b/tests/ref/code/let.png diff --git a/tests/ref/code/while.png b/tests/ref/code/while.png Binary files differindex a772fd59..0956fe45 100644 --- a/tests/ref/code/while.png +++ b/tests/ref/code/while.png diff --git a/tests/ref/insert/circle.png b/tests/ref/insert/circle.png Binary files differindex 8364d42b..7e44907b 100644 --- a/tests/ref/insert/circle.png +++ b/tests/ref/insert/circle.png diff --git a/tests/ref/insert/square.png b/tests/ref/insert/square.png Binary files differindex def2e664..c84e6f0e 100644 --- a/tests/ref/insert/square.png +++ b/tests/ref/insert/square.png diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png Binary files differindex 2205809a..b2b8707d 100644 --- a/tests/ref/layout/spacing.png +++ b/tests/ref/layout/spacing.png diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png Binary files differindex 686471c6..227b1eeb 100644 --- a/tests/ref/markup/escape.png +++ b/tests/ref/markup/escape.png diff --git a/tests/ref/markup/raw.png b/tests/ref/markup/raw.png Binary files differindex 05aeea13..f7bb2ab0 100644 --- a/tests/ref/markup/raw.png +++ b/tests/ref/markup/raw.png diff --git a/tests/ref/text/decorations.png b/tests/ref/text/decorations.png Binary files differindex e3ca1be5..5ae569f3 100644 --- a/tests/ref/text/decorations.png +++ b/tests/ref/text/decorations.png diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png Binary files differindex 27a67586..764c7413 100644 --- a/tests/ref/text/whitespace.png +++ b/tests/ref/text/whitespace.png diff --git a/tests/typ/code/array.typ b/tests/typ/code/array.typ index 9e14bf16..fc8795c2 100644 --- a/tests/typ/code/array.typ +++ b/tests/typ/code/array.typ @@ -18,6 +18,7 @@ , rgb("002") ,)} +--- // Error: 3 expected closing paren {(} diff --git a/tests/typ/code/block-invalid.typ b/tests/typ/code/block-invalid.typ deleted file mode 100644 index 01df81d5..00000000 --- a/tests/typ/code/block-invalid.typ +++ /dev/null @@ -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 -} diff --git a/tests/typ/code/block-scoping.typ b/tests/typ/code/block-scoping.typ deleted file mode 100644 index 7970ee1b..00000000 --- a/tests/typ/code/block-scoping.typ +++ /dev/null @@ -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") -} diff --git a/tests/typ/code/block.typ b/tests/typ/code/block.typ index 8c30fa64..ac289abf 100644 --- a/tests/typ/code/block.typ +++ b/tests/typ/code/block.typ @@ -1,58 +1,135 @@ // Test code blocks. +// Ref: false --- -All none - -// Nothing evaluates to none. -{} +// Ref: true -// Let evaluates to none. -{ let v = 0 } +// Evaluates to join of none, [My ] and the two loop bodies. +{ + let parts = ("my fri", "end.") + [Hello, ] + for s in parts [{s}] +} -// Type is joined with trailing none, evaluates to string. +// Evaluates to join of the templates and strings. { - type("") - none + [How] + if true { + " are" + } + [ ] + if false [Nope] + [you] + "?" } --- +// Nothing evaluates to none. +#test({}, none) + +// Let evaluates to none. +#test({ let v = 0 }, none) + // Evaluates to single expression. -{ "Hello" } +#test({ "hello" }, "hello") // Evaluates to string. -{ let x = "Hel"; x + "lo" } +#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") -// Evaluates to join of none, [He] and the two loop bodies. +--- +// Some things can't be joined. { - let parts = ("l", "lo") - [He] - for s in parts [{s}] + [A] + // Error: 5-6 cannot join template with integer + 1 + [B] } --- -// Evaluates to join of the templates and strings. +// 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. { - [Hey, ] - if true { - "there!" + let a = "a1" + { + let a = "a2" + { + test(a, "a2") + let a = "a3" + test(a, "a3") + } + test(a, "a2") } - [ ] - if false [Nope] - [How are ] + "you?" + 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`. { - [A] - // Error: 5-6 cannot join template with integer - 1 - [B] + // Error: 9-12 expected identifier, found string + for "v" + + // Error: 10 expected keyword `in` + for v let z = 1 + 2 + + z } --- -// Works the same way in code environment. -// Ref: false -#test(3, { - let x = 1 - let y = 2 - x + y -}) +// Error: 2:1 expected closing brace +{ + +--- +// Error: 1-2 unexpected closing brace +} diff --git a/tests/typ/code/call-invalid.typ b/tests/typ/code/call-invalid.typ deleted file mode 100644 index dd5897b8..00000000 --- a/tests/typ/code/call-invalid.typ +++ /dev/null @@ -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("] diff --git a/tests/typ/code/call-wide.typ b/tests/typ/code/call-wide.typ deleted file mode 100644 index 1ad4995d..00000000 --- a/tests/typ/code/call-wide.typ +++ /dev/null @@ -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! diff --git a/tests/typ/code/call.typ b/tests/typ/code/call.typ index eb5c6732..28ce860c 100644 --- a/tests/typ/code/call.typ +++ b/tests/typ/code/call.typ @@ -1,34 +1,64 @@ // Test function calls. +// Ref: false --- -// One argument. -#args(bold) +// Ref: true -// One argument and trailing comma. -#args(1,) +// Ommitted space. +#font(weight:bold)[Bold] -// One named argument. -#args(a:2) +// Call return value of function with body. +#let f(x, body) = (y) => [#x] + body + [#y] +#f(1)[2](3) -// Mixed arguments. -{args(1, b: "2", 3)} +// Don't parse this as a function. +// Should output `<function test> (it)`. +#test (it) -// Should output `() + 2`. -#args() + 2 +#let f(body) = body +#f[A] +#f()[A] +#f([A]) --- -// Ref: false +// 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. #let alias = type #test(alias(alias), "function") ---- // Callee expressions. { - // Error: 5-9 expected function, found boolean - true() - // Wrapped in parens. test((type)("hi"), "string") @@ -37,30 +67,60 @@ test(adder(2)(5), 7) } -#let f(x, body) = (y) => { - [{x}] + body + [{y}] -} - -// Call return value of function with body. -#f(1)[2](3) +--- +// Error: 2-6 expected function, found boolean +{true()} -// Don't allow this to be a closure. -// Should output `x => "hi"`. +--- #let x = "x" -#x => "hi" + +// Error: 1-3 expected function, found string +#x() + +--- +#let f(x) = x + +// Error: 1-6 expected function, found integer +#f(1)(2) + +--- +#let f(x) = x + +// Error: 1-6 expected function, found template +#f[1](2) --- -// Different forms of template arguments. +// 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*/) -#let a = "a" +// Error: 8 expected comma +#func(1 2) -#args(a) \ -#args[a] \ -#args(a, [b]) +// Error: 7-8 expected identifier +// Error: 9 expected expression +#func(1:) -// Template can be argument or body depending on whitespace. -#if "template" == type[b] [Sure ] -#if "template" == type [Nope.] #else [thing.] +// Error: 7-8 expected identifier +#func(1:2) -// Should output `<function args> (Okay.)`. -#args (Okay.) +// 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("] diff --git a/tests/typ/code/closure.typ b/tests/typ/code/closure.typ index 20a5f18d..75241f32 100644 --- a/tests/typ/code/closure.typ +++ b/tests/typ/code/closure.typ @@ -2,13 +2,22 @@ // Ref: false --- +// Don't parse closure directly in template. +// Ref: true +#let x = "\"hi\"" + +// Should output `"hi" => "bye"`. +#x => "bye" + +--- // Basic closure without captures. { let adder = (x, y) => x + y test(adder(2, 3), 5) } +--- // Pass closure as argument and return closure. // Also uses shorthand syntax for a single argument. { @@ -19,6 +28,7 @@ test(h(2), 5) } +--- // Capture environment. { let mark = "?" @@ -35,15 +45,7 @@ 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. { let x = 1 @@ -55,6 +57,15 @@ } --- +// Don't leak environment. +{ + // Error: 18-19 unknown variable + let func() = x + let x = "hi" + func() +} + +--- // Too few arguments. { let types(x, y) = "[" + type(x) + ", " + type(y) + "]" @@ -64,11 +75,11 @@ test(types("nope"), "[string, none]") } +--- // Too many arguments. { let f(x) = x + 1 // Error: 10-15 unexpected argument - // Error: 17-24 unexpected argument f(1, "two", () => x) } diff --git a/tests/typ/code/for-pattern.typ b/tests/typ/code/for-pattern.typ deleted file mode 100644 index a6a7c16a..00000000 --- a/tests/typ/code/for-pattern.typ +++ /dev/null @@ -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 -} diff --git a/tests/typ/code/for.typ b/tests/typ/code/for.typ index e6bcf269..62cb0bb5 100644 --- a/tests/typ/code/for.typ +++ b/tests/typ/code/for.typ @@ -1,42 +1,23 @@ // Test for loops. +// Ref: false --- +// Ref: true + // Empty array. #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. -// Should output `age: 1, name: Typst,`. +// Should output `Age: 2. Name: Typst.`. #for k, v in (Name: "Typst", Age: 2) [ {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. -// 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 [, ] [#v] if v == 1 [st] @@ -48,30 +29,60 @@ } // 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) }] --- -// Value of for loops. -// 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)) + +// 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(type(for v in "1" []), "template") --- -// Ref: false - // Uniterable expression. // Error: 11-15 cannot loop over boolean #for v in true {} -// Make sure that we don't complain twice. -// Error: 11-18 cannot add integer and string -#for v in 1 + "2" {} - -// Errors taint everything. -#test(error, for v in (1, 2, 3) { - if v < 2 [Ok] else {error} -}) +--- +// Keys and values of strings. +// Error: 6-10 mismatched pattern +#for k, v in "hi" { + dont-care +} --- // Error: 5 expected identifier @@ -89,19 +100,15 @@ // Error: 15 expected body #for v in iter -// Should output `v in iter`. // Error: 5 expected identifier #for v in iter {} -// Should output `A thing`. // Error: 7-10 expected identifier, found string A#for "v" thing -// Should output `in iter`. // Error: 6-9 expected identifier, found string #for "v" in iter {} -// Should output `+ b in iter`. // Error: 7 expected keyword `in` #for a + b in iter {} diff --git a/tests/typ/code/if.typ b/tests/typ/code/if.typ index dd5d23a0..db8b059e 100644 --- a/tests/typ/code/if.typ +++ b/tests/typ/code/if.typ @@ -39,9 +39,16 @@ "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. // Ref: false + { let x = 1 let y = 2 @@ -61,13 +68,12 @@ } --- -// Ref: false - // Condition must be boolean. // If it isn't, neither branch is evaluated. // Error: 5-14 expected boolean, found string #if "a" + "b" { nope } #else { nope } +--- // Make sure that we don't complain twice. // Error: 5-12 cannot add integer and string #if 1 + "2" {} diff --git a/tests/typ/code/import.typ b/tests/typ/code/import.typ index e1af8ceb..93d4d168 100644 --- a/tests/typ/code/import.typ +++ b/tests/typ/code/import.typ @@ -11,8 +11,7 @@ #let value = [foo] // 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!] #value @@ -24,10 +23,6 @@ #test(b, 1) -// This should not exist yet -// Error: 1-3 unknown variable -#d - // A wildcard import. #import * from "target.typ" @@ -45,30 +40,35 @@ #import a, c, from "target.typ" --- -// Test bad imports. -// Ref: false - // Error: 19-21 file not found #import name from "" +--- // Error: 16-27 file not found #import * from "lib/0.2.1" +--- // Some non-text stuff. // Error: 16-37 file is not valid utf-8 #import * from "../../res/rhino.png" +--- // Unresolved import. // Error: 9-21 unresolved import #import non_existing from "target.typ" -// Cyclic import. -// Error: 16-41 cyclic import -#import * from "./importable/cycle1.typ" +--- +// Cyclic import of this very file. +// 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 keyword `from` #import @@ -100,7 +100,6 @@ #from "target.typ" // Should output `target`. -// Error: 1:16-2:2 file not found // Error: 2:2 expected semicolon or line break #import * from "target.typ "target diff --git a/tests/typ/code/importable/cycle1.typ b/tests/typ/code/importable/cycle1.typ index ae755fa0..a9c00f5e 100644 --- a/tests/typ/code/importable/cycle1.typ +++ b/tests/typ/code/importable/cycle1.typ @@ -1,6 +1,5 @@ // Ref: false -// Error: 16-28 cyclic import #import * from "cycle2.typ" #let inaccessible = "wow" diff --git a/tests/typ/code/importable/cycle2.typ b/tests/typ/code/importable/cycle2.typ index d4f94564..204da519 100644 --- a/tests/typ/code/importable/cycle2.typ +++ b/tests/typ/code/importable/cycle2.typ @@ -1,6 +1,5 @@ // Ref: false -// Error: 16-28 cyclic import #import * from "cycle1.typ" #let val = "much cycle" diff --git a/tests/typ/code/include.typ b/tests/typ/code/include.typ index 166c3945..0d1abc08 100644 --- a/tests/typ/code/include.typ +++ b/tests/typ/code/include.typ @@ -6,18 +6,21 @@ // Include a file #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. #let chap2 = include "import" + "able/chap" + "2.typ" -- _Intermission_ -- #chap2 +--- { - // Expressions, code mode. // Error: 21-43 file not found 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 diff --git a/tests/typ/code/let.typ b/tests/typ/code/let.typ index 9079b541..3f3f9d35 100644 --- a/tests/typ/code/let.typ +++ b/tests/typ/code/let.typ @@ -1,38 +1,19 @@ // Test let bindings. --- -// Ref: false - // Automatically initialized with none. #let x #test(x, none) -// Error: 9 expected expression -#let y = -#test(y, none) - // Manually initialized with one. #let z = 1 #test(z, 1) ---- // Syntax sugar for function definitions. #let fill = conifer #let rect(body) = rect(width: 2cm, fill: fill, pad(5pt, body)) #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. @@ -47,20 +28,9 @@ One #let v3 = 3; 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(v2, 2) #test(v3, 3) -#test(v4, 4) -#test(v5, (1, 2)) --- // Error: 5 expected identifier @@ -72,13 +42,27 @@ Three // Error: 6-9 expected identifier, found string #let "v" -// Should output `1`. // Error: 7 expected semicolon or line break #let v 1 // Error: 9 expected expression #let v = -// Should output a heading `1`. // Error: 6-9 expected identifier, found string #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) = diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ index 149a60dd..5e56ff98 100644 --- a/tests/typ/code/ops-invalid.typ +++ b/tests/typ/code/ops-invalid.typ @@ -1,67 +1,84 @@ -// Test invalid expressions. +// Test invalid operations. // Ref: false --- -// Missing expressions. - // Error: 3 expected expression {-} +--- // Error: 10 expected expression #test({1+}, 1) +--- // Error: 10 expected expression #test({2*}, 2) --- -// Mismatched types. - // Error: 2-12 cannot apply '+' to template {+([] + [])} +--- // Error: 2-5 cannot apply '-' to string {-""} +--- // Error: 2-8 cannot apply 'not' to array {not ()} +--- // Error: 2-12 cannot apply '<=' to relative and relative {30% <= 40%} +--- // Special messages for +, -, * and /. // Error: 03-10 cannot add integer and string +{(1 + "2", 40% - 1)} + +--- // Error: 12-19 cannot subtract integer from relative -// Error: 21-29 cannot multiply integer with boolean -// Error: 31-39 cannot divide integer by length -{(1 + "2", 40% - 1, 2 * true, 3 / 12pt)} +{(1234567, 40% - 1)} + +--- +// Error: 2-10 cannot multiply integer with boolean +{2 * true} -// Error: 14-22 cannot apply '+=' to integer and string +--- +// Error: 2-10 cannot divide integer by length +{3 / 12pt} + +--- +// Error: 14-22 cannot add integer and string { let x = 1; x += "2" } +--- // Error: 13-14 expected argument list, found integer { test with 2 } +--- // Error: 3-4 expected function, found integer { 1 with () } +--- // Error: 3-10 cannot apply '..' to integer and string { 1 .. "" } --- -// Bad left-hand sides of assignment. - // Error: 3-6 cannot assign to this expression { (x) = "" } +--- // Error: 3-8 cannot assign to this expression { 1 + 2 += 3 } +--- // Error: 3-4 unknown variable { z = 1 } +--- // Error: 3-7 cannot assign to a constant { rect = "hi" } +--- // Works if we define rect beforehand // (since then it doesn't resolve to the standard library version anymore). #let rect = "" diff --git a/tests/typ/code/repr.typ b/tests/typ/code/repr.typ index 3da86bf8..35a47e49 100644 --- a/tests/typ/code/repr.typ +++ b/tests/typ/code/repr.typ @@ -10,9 +10,6 @@ {ke-bab} \ {α} -// Error: 2-3 unknown variable -{_} - --- // Literal values. {none} (empty) \ diff --git a/tests/typ/code/while.typ b/tests/typ/code/while.typ index 306c1e45..2f0984d2 100644 --- a/tests/typ/code/while.typ +++ b/tests/typ/code/while.typ @@ -29,23 +29,16 @@ #test(type(while i < 1 [{ i += 1 }]), "template") --- -// Ref: false - // Condition must be boolean. // Error: 8-14 expected boolean, found template #while [nope] [nope] -// Make sure that we don't complain twice. -// Error: 8-15 unknown variable -#while nothing {} - -// Errors taint everything. -#let i = 0 -#test(error, while i < 10 { - i += 1 - if i < 5 [nope] else { error } -}) -#test(i, 10) +--- +// Make sure that we terminate and don't complain multiple times. +#while true { + // Error: 5-9 unknown variable + nope +} --- // Error: 7 expected expression @@ -57,11 +50,9 @@ // Error: 9 expected body #while x -// Should output `x`. // Error: 7 expected expression #while x {} -// Should output `something`. // Error: 9 expected body #while x something diff --git a/tests/typ/insert/circle.typ b/tests/typ/insert/circle.typ index 38fce645..8d76f4ef 100644 --- a/tests/typ/insert/circle.typ +++ b/tests/typ/insert/circle.typ @@ -39,9 +39,9 @@ Expanded by height. --- // Radius wins over width and height. // Error: 23-34 unexpected argument -// Error: 36-49 unexpected argument #circle(radius: 10pt, width: 50pt, height: 100pt, fill: eastern) +--- // Width wins over height. -// Error: 22-34 unexpected argument -#circle(width: 20pt, height: 50pt, fill: eastern) +// Error: 9-21 unexpected argument +#circle(height: 50pt, width: 20pt, fill: eastern) diff --git a/tests/typ/insert/image.typ b/tests/typ/insert/image.typ index 35087c2e..84069949 100644 --- a/tests/typ/insert/image.typ +++ b/tests/typ/insert/image.typ @@ -10,12 +10,6 @@ // Load an RGB JPEG image. #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. @@ -36,3 +30,11 @@ // Make sure the bounding-box of the image is correct. #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") diff --git a/tests/typ/insert/square.typ b/tests/typ/insert/square.typ index d546773f..7d65b529 100644 --- a/tests/typ/insert/square.typ +++ b/tests/typ/insert/square.typ @@ -10,12 +10,6 @@ Auto-sized square. \ ] --- -// 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. #page!(width: 75pt, height: 100pt) #square(fill: conifer)[ @@ -28,3 +22,8 @@ Auto-sized square. \ #square(fill: conifer)[ 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")) diff --git a/tests/typ/layout/grid-3.typ b/tests/typ/layout/grid-3.typ index 38572520..450ce60c 100644 --- a/tests/typ/layout/grid-3.typ +++ b/tests/typ/layout/grid-3.typ @@ -25,9 +25,9 @@ gutter-columns: (0pt, 10%), image("../../res/rhino.png"), align(right, rect(width: 100%, fill: eastern)[LoL]), - "rofl", - "\nA" * 3, - "Ha!\n" * 3, + [rofl], + [\ A] * 3, + [Ha!\ ] * 3, ) --- @@ -38,9 +38,9 @@ gutter-rows: (8pt,), gutter-columns: (0pt, 10%), [A], [B], [C], - "Ha!\n" * 6, - "rofl", - "\nA" * 3, + [Ha!\ ] * 6, + [rofl], + [\ A] * 3, [hello], [darkness], [my old] @@ -54,10 +54,10 @@ gutter-rows: (10pt,), gutter-columns: (0pt, 10%), [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]), - "rofl", - "E\n"*4, + [rofl], + [E\ ]*4, ) --- diff --git a/tests/typ/layout/pad.typ b/tests/typ/layout/pad.typ index d43cafc5..eba68af2 100644 --- a/tests/typ/layout/pad.typ +++ b/tests/typ/layout/pad.typ @@ -10,8 +10,7 @@ #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) diff --git a/tests/typ/layout/page.typ b/tests/typ/layout/page.typ index d154d5f4..ba87ad51 100644 --- a/tests/typ/layout/page.typ +++ b/tests/typ/layout/page.typ @@ -22,9 +22,6 @@ // Ensure that specific margins override general margins. #page(margins: 0pt, left: 20pt)[Overriden] -// Error: 8-19 unknown variable -#page!(nonexistant) - // Flipped predefined paper. #page("a11", flip: true)[Flipped A11] diff --git a/tests/typ/layout/pagebreak.typ b/tests/typ/layout/pagebreak.typ index b13a1bfd..ff15d156 100644 --- a/tests/typ/layout/pagebreak.typ +++ b/tests/typ/layout/pagebreak.typ @@ -10,10 +10,7 @@ First of two A #box[ B - // Error: 16 cannot modify page from here #pagebreak() - - // Error: 11-15 cannot modify page from here #page("a4")[] ] C diff --git a/tests/typ/layout/spacing.typ b/tests/typ/layout/spacing.typ index bd38631e..ec520063 100644 --- a/tests/typ/layout/spacing.typ +++ b/tests/typ/layout/spacing.typ @@ -13,6 +13,7 @@ Add #h(10pt) #h(10pt) up // Relative to font size. Relative #h(100%) spacing +--- // Missing spacing. // Error: 12 missing argument: spacing Totally #h() ignored diff --git a/tests/typ/markup/escape.typ b/tests/typ/markup/escape.typ index b0a809f9..c039e0b7 100644 --- a/tests/typ/markup/escape.typ +++ b/tests/typ/markup/escape.typ @@ -21,14 +21,15 @@ // Escaped escape sequence. \u{41} vs. \\u\{41\} +// Some code stuff in text. +let f() , ; : | + - /= == 12 "string" + +--- // Unicode codepoint does not exist. // Error: 1-11 invalid unicode escape sequence \u{FFFFFF} +--- // Unterminated. // Error: 6 expected closing brace \u{41*Bold* - ---- -// Some code stuff in text. -let f() , ; : | + - /= == 12 "string" diff --git a/tests/typ/text/decorations.typ b/tests/typ/text/decorations.typ index 3741dc19..83b04650 100644 --- a/tests/typ/text/decorations.typ +++ b/tests/typ/text/decorations.typ @@ -1,19 +1,34 @@ // Test text decorations. --- -#strike[Statements dreamt up by the utterly deranged.] - -Sometimes, we work #strike(10pt, extent: 5%)[in secret]. -There might be #strike(stroke: rgb("abcdef88"), thickness: 10pt, extent: 5%)[redacted] -things. +// Basic strikethrough. +#strike[ + Statements dreamt up by the utterly deranged. +] +// Move underline down. #underline(offset: 5pt)[Further below.] ---- +// Different color. #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.]) ---- +// Both over- and underline. #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. diff --git a/tests/typ/text/font.typ b/tests/typ/text/font.typ index 0f64e244..cc906fbc 100644 --- a/tests/typ/text/font.typ +++ b/tests/typ/text/font.typ @@ -54,21 +54,25 @@ Emoji: 🐪, 🌋, 🏞 #font(monospace, monospace: ("Nope", "Latin Modern Math"))[Math.] --- -// Ref: false - // Error: 7-12 unexpected argument #font(false)[] +--- // Error: 14-18 expected font style, found font weight -// Error: 28-34 expected font weight, found string -// Error: 43-44 expected string or array of strings, found integer -#font(style: bold, weight: "thin", serif: 0)[] +#font(style: bold, weight: "thin")[] -// Error: 7-27 unexpected argument -#font(something: "invalid")[] +--- +// Error: 14-15 expected string or array of strings, found integer +#font(serif: 0)[] -// Error: 13-23 unexpected argument -#font(12pt, size: 10pt)[] +--- +// Error: 19-23 unexpected argument +#font(size: 10pt, 12pt)[] -// Error: 16-35 unexpected argument -#font("Arial", family: "Helvetica")[] +--- +// Error: 28-35 unexpected argument +#font(family: "Helvetica", "Arial")[] + +--- +// Error: 7-27 unexpected argument +#font(something: "invalid")[] diff --git a/tests/typ/text/whitespace.typ b/tests/typ/text/whitespace.typ index 418c3a12..c81513fa 100644 --- a/tests/typ/text/whitespace.typ +++ b/tests/typ/text/whitespace.typ @@ -2,16 +2,12 @@ --- // Spacing around let. - -// Error: 6 expected identifier -A#let;B \ A#let x = 1;B #test(x, 1) \ A #let x = 2;B #test(x, 2) \ A#let x = 3; B #test(x, 3) --- // Spacing around if-else. - 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. - #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. - A#for _ in (none,) [B]C \ A#for _ in (none,) [B] C \ A #for _ in (none,) {"B"}C diff --git a/tests/typ/utility/basics.typ b/tests/typ/utility/basics.typ index 75e06413..25cac039 100644 --- a/tests/typ/utility/basics.typ +++ b/tests/typ/utility/basics.typ @@ -3,14 +3,15 @@ --- // Test the `len` function. - #test(len(()), 0) #test(len(("A", "B", "C")), 3) #test(len("Hello World!"), 12) #test(len((a: 1, b: 2)), 2) +--- // Error: 6 missing argument: collection #len() +--- // Error: 6-10 expected string, array or dictionary #len(12pt) diff --git a/tests/typ/utility/color.typ b/tests/typ/utility/color.typ index 8f4c0522..5b10477f 100644 --- a/tests/typ/utility/color.typ +++ b/tests/typ/utility/color.typ @@ -11,13 +11,14 @@ // Clamped. #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: 11 missing argument: blue component -#test(rgb(), black) +--- +// Error: 6-10 missing argument: blue component +#rgb(0, 1) diff --git a/tests/typ/utility/math.typ b/tests/typ/utility/math.typ index db234d9c..933f882f 100644 --- a/tests/typ/utility/math.typ +++ b/tests/typ/utility/math.typ @@ -8,8 +8,10 @@ #test(max(-3, 11), 11) #test(min("hi"), "hi") +--- // Error: 6 missing argument: value #min() +--- // Error: 11-18 cannot compare integer with string #test(min(1, "hi"), error) diff --git a/tests/typeset.rs b/tests/typeset.rs index 78577098..39ff9643 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::env; use std::ffi::OsStr; use std::fs; @@ -11,20 +10,17 @@ use ttf_parser::{GlyphId, OutlineBuilder}; use walkdir::WalkDir; use typst::color::Color; -use typst::diag::{Diag, DiagSet, Level}; -use typst::eval::{eval, Scope, Value}; +use typst::diag::{Error, TypResult}; +use typst::eval::{eval, Value}; use typst::exec::{exec, State}; use typst::geom::{self, Length, PathElement, Point, Sides, Size}; 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::parse::{parse, LineMap, Scanner}; use typst::syntax::{Location, Pos}; use typst::Context; -#[cfg(feature = "layout-cache")] -use typst::layout::LayoutTree; - const TYP_DIR: &str = "./typ"; const REF_DIR: &str = "./ref"; const PNG_DIR: &str = "./png"; @@ -67,10 +63,22 @@ fn main() { page.size = Size::new(Length::pt(120.0), Length::inf()); 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 panics = Rc::new(RefCell::new(vec![])); - register_helpers(&mut std, Rc::clone(&panics)); + std.def_func("test", move |_, args| { + 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. let loader = FsLoader::new().with_path(FONT_DIR).wrap(); @@ -88,7 +96,6 @@ fn main() { ok &= test( &mut ctx, loader.as_ref(), - &panics, &src_path, &png_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( ctx: &mut Context, loader: &FsLoader, - panics: &Panics, src_path: &Path, png_path: &Path, ref_path: &Path, @@ -171,8 +152,8 @@ fn test( let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path); println!("Testing {}", name.display()); + let file = loader.resolve(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 frames = vec![]; @@ -196,7 +177,7 @@ fn test( } } else { 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; compare_ever |= compare_here; frames.extend(part_frames); @@ -221,7 +202,7 @@ fn test( println!(" Does not match reference image. ❌"); ok = false; } - } else { + } else if !frames.is_empty() { println!(" Failed to open reference image. ❌"); ok = false; } @@ -236,71 +217,58 @@ fn test( fn test_part( ctx: &mut Context, - panics: &Panics, - src_id: FileId, + file: FileId, src: &str, i: usize, compare_ref: bool, lines: u32, ) -> (bool, bool, Vec<Rc<Frame>>) { 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); - // 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 ok = true; - let mut diags = ast.diags; - diags.extend(module.diags); - diags.extend(tree.diags); + let result = typeset(ctx, file, src); + let (frames, mut errors) = match result { + #[allow(unused_variables)] + Ok((tree, mut frames)) => { + #[cfg(feature = "layout-cache")] + (ok &= test_incremental(ctx, i, &tree, &frames)); - let mut ok = true; + if !compare_ref { + frames.clear(); + } - for panic in panics.borrow().iter() { - let line = map.location(panic.pos).unwrap().line; - println!(" Assertion failed in line {} ❌", lines + line); - if let (Some(lhs), Some(rhs)) = (&panic.lhs, &panic.rhs) { - println!(" Left: {:?}", lhs); - println!(" Right: {:?}", rhs); - } else { - println!(" Missing argument."); + (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 { - println!(" Subtest {} does not match expected diagnostics. ❌", i); + if errors != ref_errors { + println!(" Subtest {} does not match expected errors. ❌", i); ok = false; - for diag in &diags { - if !ref_diags.contains(diag) { + for error in errors.iter() { + if error.file == file && !ref_errors.contains(error) { print!(" Not annotated | "); - print_diag(diag, &map, lines); + print_error(error, &map, lines); } } - for diag in &ref_diags { - if !diags.contains(diag) { + for error in ref_errors.iter() { + if !errors.contains(error) { 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) } @@ -350,9 +318,9 @@ fn test_incremental( ok } -fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) { - let mut diags = DiagSet::new(); +fn parse_metadata(file: FileId, src: &str, map: &LineMap) -> (Option<bool>, Vec<Error>) { let mut compare_ref = None; + let mut errors = vec![]; let lines: Vec<_> = src.lines().map(str::trim).collect(); 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); } - let (level, rest) = if let Some(rest) = line.strip_prefix("// Warning: ") { - (Level::Warning, rest) - } else if let Some(rest) = line.strip_prefix("// Error: ") { - (Level::Error, rest) + let rest = if let Some(rest) = line.strip_prefix("// Error: ") { + rest } else { continue; }; @@ -391,18 +357,30 @@ fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) { let start = pos(&mut s); 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 typeset( + ctx: &mut Context, + 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_diag(diag: &Diag, map: &LineMap, lines: u32) { - let mut start = map.location(diag.span.start).unwrap(); - let mut end = map.location(diag.span.end).unwrap(); +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; 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 { |
