From eaa3cbaa9c2b1564a4b0db013672245a1893314a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 12 Aug 2021 13:39:33 +0200 Subject: [PATCH] Array and dictionary indexing --- src/diag.rs | 11 +- src/eval/array.rs | 52 ++++--- src/eval/dict.rs | 25 +++- src/eval/function.rs | 39 ++++- src/eval/mod.rs | 253 ++++++++++++++++++++------------- src/eval/ops.rs | 12 +- src/eval/template.rs | 17 ++- src/font.rs | 2 +- src/library/utility.rs | 4 +- src/parse/scanner.rs | 9 +- src/parse/tokens.rs | 6 +- src/pretty.rs | 14 +- src/syntax/visit.rs | 1 - src/util/eco.rs | 36 +++-- src/util/mod.rs | 21 +++ tests/typ/code/array.typ | 36 +++++ tests/typ/code/call.typ | 8 +- tests/typ/code/dict.typ | 28 ++++ tests/typ/code/ops-invalid.typ | 20 ++- tests/typ/code/ops-prec.typ | 2 +- tests/typeset.rs | 8 +- 21 files changed, 416 insertions(+), 188 deletions(-) diff --git a/src/diag.rs b/src/diag.rs index 0fcdef173..1905d4807 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -58,21 +58,20 @@ impl Error { Box::new(vec![Self::new(source, span, message)]) } - /// Partially build a vec-boxed error, returning a function that just needs - /// the message. + /// Create a closure that contains the positional information for an error + /// and just needs the message to yield a vec-boxed error. /// /// This is useful in to convert from [`StrResult`] to a [`TypResult`] using /// [`map_err`](Result::map_err). - pub fn partial( + pub fn at>( source: SourceId, span: impl Into, - ) -> impl FnOnce(String) -> Box> { + ) -> impl FnOnce(S) -> Box> { move |message| Self::boxed(source, span, message) } } /// Early-return with a vec-boxed [`Error`]. -#[macro_export] macro_rules! bail { ($source:expr, $span:expr, $message:expr $(,)?) => { return Err(Box::new(vec![$crate::diag::Error::new( @@ -81,6 +80,6 @@ macro_rules! bail { }; ($source:expr, $span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { - $crate::bail!($source, $span, format!($fmt, $($arg),+)); + bail!($source, $span, format!($fmt, $($arg),+)); }; } diff --git a/src/eval/array.rs b/src/eval/array.rs index 38876c960..356aa0ca9 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -1,12 +1,14 @@ +use std::convert::TryFrom; use std::fmt::{self, Debug, Formatter}; use std::iter::FromIterator; use std::ops::{Add, AddAssign}; use std::rc::Rc; use super::Value; +use crate::diag::StrResult; /// Create a new [`Array`] from values. -#[macro_export] +#[allow(unused_macros)] macro_rules! array { ($value:expr; $count:expr) => { $crate::eval::Array::from_vec(vec![$crate::eval::Value::from($value); $count]) @@ -47,25 +49,25 @@ impl Array { } /// The length of the array. - pub fn len(&self) -> usize { - self.vec.len() + pub fn len(&self) -> i64 { + self.vec.len() as i64 } /// Borrow the value at the given index. - pub fn get(&self, index: usize) -> Option<&Value> { - self.vec.get(index) + pub fn get(&self, index: i64) -> StrResult<&Value> { + usize::try_from(index) + .ok() + .and_then(|i| self.vec.get(i)) + .ok_or_else(|| out_of_bounds(index, self.len())) } /// Mutably borrow the value at the given index. - pub fn get_mut(&mut self, index: usize) -> Option<&mut Value> { - Rc::make_mut(&mut self.vec).get_mut(index) - } - - /// Set the value at the given index. - /// - /// This panics the `index` is out of range. - pub fn set(&mut self, index: usize, value: Value) { - Rc::make_mut(&mut self.vec)[index] = value; + pub fn get_mut(&mut self, index: i64) -> StrResult<&mut Value> { + let len = self.len(); + usize::try_from(index) + .ok() + .and_then(move |i| Rc::make_mut(&mut self.vec).get_mut(i)) + .ok_or_else(|| out_of_bounds(index, len)) } /// Push a value to the end of the array. @@ -82,16 +84,26 @@ impl Array { } } - /// Repeat this array `n` times. - pub fn repeat(&self, n: usize) -> Self { - let len = self.len().checked_mul(n).expect("capacity overflow"); - self.iter().cloned().cycle().take(len).collect() - } - /// Iterate over references to the contained values. pub fn iter(&self) -> std::slice::Iter { self.vec.iter() } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: i64) -> StrResult { + let count = usize::try_from(n) + .ok() + .and_then(|n| self.vec.len().checked_mul(n)) + .ok_or_else(|| format!("cannot repeat this array {} times", n))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: i64) -> String { + format!("array index out of bounds (index: {}, len: {})", index, len) } impl Default for Array { diff --git a/src/eval/dict.rs b/src/eval/dict.rs index bc7df2b8f..730392dc8 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -5,10 +5,12 @@ use std::ops::{Add, AddAssign}; use std::rc::Rc; use super::Value; +use crate::diag::StrResult; +use crate::pretty::pretty; use crate::util::EcoString; /// Create a new [`Dict`] from key-value pairs. -#[macro_export] +#[allow(unused_macros)] macro_rules! dict { ($($key:expr => $value:expr),* $(,)?) => {{ #[allow(unused_mut)] @@ -41,18 +43,21 @@ impl Dict { } /// The number of pairs in the dictionary. - pub fn len(&self) -> usize { - self.map.len() + pub fn len(&self) -> i64 { + self.map.len() as i64 } /// Borrow the value the given `key` maps to. - pub fn get(&self, key: &str) -> Option<&Value> { - self.map.get(key) + pub fn get(&self, key: &str) -> StrResult<&Value> { + self.map.get(key).ok_or_else(|| missing_key(key)) } /// Mutably borrow the value the given `key` maps to. - pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> { - Rc::make_mut(&mut self.map).get_mut(key) + /// + /// This inserts the key with [`None`](Value::None) as the value if not + /// present so far. + pub fn get_mut(&mut self, key: EcoString) -> &mut Value { + Rc::make_mut(&mut self.map).entry(key).or_default() } /// Insert a mapping from the given `key` to the given `value`. @@ -75,6 +80,12 @@ impl Dict { } } +/// The missing key access error message. +#[cold] +fn missing_key(key: &str) -> String { + format!("dictionary does not contain key: {}", pretty(key)) +} + impl Default for Dict { fn default() -> Self { Self::new() diff --git a/src/eval/function.rs b/src/eval/function.rs index 8b1e883f0..be7894f43 100644 --- a/src/eval/function.rs +++ b/src/eval/function.rs @@ -134,7 +134,7 @@ impl FuncArgs { let value = self.items.remove(index).value; let span = value.span; - T::cast(value).map(Some).map_err(Error::partial(self.source, span)) + T::cast(value).map(Some).map_err(Error::at(self.source, span)) } /// Return an "unexpected argument" error if there is any remaining @@ -146,3 +146,40 @@ impl FuncArgs { Ok(()) } } + +impl FuncArgs { + /// Reinterpret these arguments as actually being an array index. + pub fn into_index(self) -> TypResult { + self.into_castable("index") + } + + /// Reinterpret these arguments as actually being a dictionary key. + pub fn into_key(self) -> TypResult { + self.into_castable("key") + } + + /// Reinterpret these arguments as actually being a single castable thing. + fn into_castable(self, what: &str) -> TypResult + where + T: Cast, + { + let mut iter = self.items.into_iter(); + let value = match iter.next() { + None => { + bail!(self.source, self.span, "missing {}", what); + } + Some(FuncArg { name: Some(_), span, .. }) => { + bail!(self.source, span, "named pair is not allowed here"); + } + Some(FuncArg { name: None, value, .. }) => { + value.v.cast().map_err(Error::at(self.source, value.span))? + } + }; + + if let Some(arg) = iter.next() { + bail!(self.source, arg.span, "only one {} is allowed", what); + } + + Ok(value) + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e911596df..abb7cab7a 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -20,6 +20,7 @@ pub use scope::*; pub use template::*; pub use value::*; +use std::cell::RefMut; use std::collections::HashMap; use std::io; use std::mem; @@ -34,7 +35,7 @@ use crate::parse::parse; use crate::source::{SourceId, SourceStore}; use crate::syntax::visit::Visit; use crate::syntax::*; -use crate::util::EcoString; +use crate::util::{EcoString, RefMutExt}; use crate::Context; /// Evaluate a parsed source file into a module. @@ -60,15 +61,6 @@ pub struct Module { pub template: Template, } -/// Evaluate an expression. -pub trait Eval { - /// The output of evaluating the expression. - type Output; - - /// Evaluate the expression to the output value. - fn eval(&self, ctx: &mut EvalContext) -> TypResult; -} - /// The context for evaluation. pub struct EvalContext<'a> { /// The loader from which resources (files and images) are loaded. @@ -168,45 +160,19 @@ impl<'a> EvalContext<'a> { } } +/// Evaluate an expression. +pub trait Eval { + /// The output of evaluating the expression. + type Output; + + /// Evaluate the expression to the output value. + fn eval(&self, ctx: &mut EvalContext) -> TypResult; +} + impl Eval for Rc { type Output = Template; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - trait Walk { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()>; - } - - impl Walk for SyntaxTree { - fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { - for node in self.iter() { - node.walk(ctx)?; - } - Ok(()) - } - } - - 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)?; @@ -302,8 +268,8 @@ impl Eval for BlockExpr { let mut output = Value::None; for expr in &self.exprs { let value = expr.eval(ctx)?; - output = ops::join(output, value) - .map_err(Error::partial(ctx.source, expr.span()))?; + output = + ops::join(output, value).map_err(Error::at(ctx.source, expr.span()))?; } if self.scoping { @@ -324,7 +290,7 @@ impl Eval for UnaryExpr { UnOp::Neg => ops::neg(value), UnOp::Not => ops::not(value), }; - result.map_err(Error::partial(ctx.source, self.span)) + result.map_err(Error::at(ctx.source, self.span)) } } @@ -371,7 +337,7 @@ impl BinaryExpr { } let rhs = self.rhs.eval(ctx)?; - op(lhs, rhs).map_err(Error::partial(ctx.source, self.span)) + op(lhs, rhs).map_err(Error::at(ctx.source, self.span)) } /// Apply an assignment operation. @@ -379,27 +345,11 @@ impl BinaryExpr { where F: FnOnce(Value, Value) -> StrResult, { - 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 => bail!(ctx.source, lspan, "unknown variable"), - } - } else { - bail!(ctx.source, lspan, "cannot assign to this expression",); - }; - + let source = ctx.source; let rhs = self.rhs.eval(ctx)?; - let mut mutable = match slot.try_borrow_mut() { - Ok(mutable) => mutable, - Err(_) => { - bail!(ctx.source, lspan, "cannot assign to a constant",); - } - }; - - let lhs = mem::take(&mut *mutable); - *mutable = op(lhs, rhs).map_err(Error::partial(ctx.source, self.span))?; - + let mut target = self.lhs.access(ctx)?; + let lhs = mem::take(&mut *target); + *target = op(lhs, rhs).map_err(Error::at(source, self.span))?; Ok(Value::None) } } @@ -408,32 +358,45 @@ impl Eval for CallExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let callee = self - .callee - .eval(ctx)? - .cast::() - .map_err(Error::partial(ctx.source, self.callee.span()))?; - + let callee = self.callee.eval(ctx)?; let mut args = self.args.eval(ctx)?; - let returned = callee(ctx, &mut args).map_err(|mut errors| { - for error in errors.iter_mut() { - // Skip errors directly related to arguments. - if error.source == ctx.source && self.span.contains(error.span) { - continue; - } - error.trace.push(( - ctx.source, - self.span, - Tracepoint::Call(callee.name().map(Into::into)), - )); + match callee { + Value::Array(array) => array + .get(args.into_index()?) + .map(Value::clone) + .map_err(Error::at(ctx.source, self.span)), + + Value::Dict(dict) => dict + .get(&args.into_key()?) + .map(Value::clone) + .map_err(Error::at(ctx.source, self.span)), + + Value::Func(func) => { + let returned = func(ctx, &mut args).map_err(|mut errors| { + for error in errors.iter_mut() { + // Skip errors directly related to arguments. + if error.source != ctx.source || !self.span.contains(error.span) { + error.trace.push(( + ctx.source, + self.span, + Tracepoint::Call(func.name().map(Into::into)), + )); + } + } + errors + })?; + args.finish()?; + Ok(returned) } - errors - })?; - args.finish()?; - - Ok(returned) + v => bail!( + ctx.source, + self.callee.span(), + "expected function or collection, found {}", + v.type_name(), + ), + } } } @@ -518,7 +481,7 @@ impl Eval for WithExpr { .callee .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.source, self.callee.span()))?; + .map_err(Error::at(ctx.source, self.callee.span()))?; let applied = self.args.eval(ctx)?; @@ -568,7 +531,7 @@ impl Eval for IfExpr { .condition .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.source, self.condition.span()))?; + .map_err(Error::at(ctx.source, self.condition.span()))?; if condition { self.if_body.eval(ctx) @@ -590,11 +553,11 @@ impl Eval for WhileExpr { .condition .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.source, self.condition.span()))? + .map_err(Error::at(ctx.source, self.condition.span()))? { let value = self.body.eval(ctx)?; output = ops::join(output, value) - .map_err(Error::partial(ctx.source, self.body.span()))?; + .map_err(Error::at(ctx.source, self.body.span()))?; } Ok(output) @@ -616,7 +579,7 @@ impl Eval for ForExpr { let value = self.body.eval(ctx)?; output = ops::join(output, value) - .map_err(Error::partial(ctx.source, self.body.span()))?; + .map_err(Error::at(ctx.source, self.body.span()))?; } ctx.scopes.exit(); @@ -662,7 +625,7 @@ impl Eval for ImportExpr { .path .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.source, self.path.span()))?; + .map_err(Error::at(ctx.source, self.path.span()))?; let file = ctx.import(&path, self.path.span())?; let module = &ctx.modules[&file]; @@ -696,7 +659,7 @@ impl Eval for IncludeExpr { .path .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.source, self.path.span()))?; + .map_err(Error::at(ctx.source, self.path.span()))?; let file = ctx.import(&path, self.path.span())?; let module = &ctx.modules[&file]; @@ -704,3 +667,97 @@ impl Eval for IncludeExpr { Ok(Value::Template(module.template.clone())) } } + +/// Walk a node in a template, filling the context's expression map. +pub trait Walk { + /// Walk the node. + fn walk(&self, ctx: &mut EvalContext) -> TypResult<()>; +} + +impl Walk for SyntaxTree { + fn walk(&self, ctx: &mut EvalContext) -> TypResult<()> { + for node in self.iter() { + node.walk(ctx)?; + } + Ok(()) + } +} + +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(()) + } +} + +/// Try to mutably access the value an expression points to. +/// +/// This only works if the expression is a valid lvalue. +pub trait Access { + /// Try to access the value. + fn access<'a>(&self, ctx: &'a mut EvalContext) -> TypResult>; +} + +impl Access for Expr { + fn access<'a>(&self, ctx: &'a mut EvalContext) -> TypResult> { + match self { + Expr::Ident(ident) => ident.access(ctx), + Expr::Call(call) => call.access(ctx), + _ => bail!( + ctx.source, + self.span(), + "cannot access this expression mutably", + ), + } + } +} + +impl Access for Ident { + fn access<'a>(&self, ctx: &'a mut EvalContext) -> TypResult> { + ctx.scopes + .get(self) + .ok_or("unknown variable") + .and_then(|slot| { + slot.try_borrow_mut().map_err(|_| "cannot mutate a constant") + }) + .map_err(Error::at(ctx.source, self.span)) + } +} + +impl Access for CallExpr { + fn access<'a>(&self, ctx: &'a mut EvalContext) -> TypResult> { + let source = ctx.source; + let args = self.args.eval(ctx)?; + let guard = self.callee.access(ctx)?; + + RefMut::try_map(guard, |value| match value { + Value::Array(array) => array + .get_mut(args.into_index()?) + .map_err(Error::at(source, self.span)), + + Value::Dict(dict) => Ok(dict.get_mut(args.into_key()?)), + + v => bail!( + source, + self.callee.span(), + "expected collection, found {}", + v.type_name(), + ), + }) + } +} diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 2bf1c1893..f79b2bf61 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -150,12 +150,12 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult { (Fractional(a), Float(b)) => Fractional(a * b), (Int(a), Fractional(b)) => Fractional(a as f64 * b), - (Str(a), Int(b)) => Str(a.repeat(b.max(0) as usize)), - (Int(a), Str(b)) => Str(b.repeat(a.max(0) as usize)), - (Array(a), Int(b)) => Array(a.repeat(b.max(0) as usize)), - (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)), + (Str(a), Int(b)) => Str(a.repeat(b)?), + (Int(a), Str(b)) => Str(b.repeat(a)?), + (Array(a), Int(b)) => Array(a.repeat(b)?), + (Int(a), Array(b)) => Array(b.repeat(a)?), + (Template(a), Int(b)) => Template(a.repeat(b)?), + (Int(a), Template(b)) => Template(b.repeat(a)?), (a, b) => mismatch!("cannot multiply {} with {}", a, b), }) diff --git a/src/eval/template.rs b/src/eval/template.rs index 29b5663de..4e20b8f8f 100644 --- a/src/eval/template.rs +++ b/src/eval/template.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; +use std::convert::TryFrom; use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, AddAssign, Deref}; use std::rc::Rc; use super::Value; +use crate::diag::StrResult; use crate::exec::ExecContext; use crate::syntax::{Expr, SyntaxTree}; use crate::util::EcoString; @@ -26,10 +28,15 @@ impl Template { } /// 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) } + pub fn repeat(&self, n: i64) -> StrResult { + let count = usize::try_from(n) + .ok() + .and_then(|n| self.nodes.len().checked_mul(n)) + .ok_or_else(|| format!("cannot repeat this template {} times", n))?; + + Ok(Self { + nodes: Rc::new(self.iter().cloned().cycle().take(count).collect()), + }) } } @@ -123,7 +130,7 @@ pub struct TemplateTree { /// The raw pointers point into the expressions contained in some /// [`SyntaxTree`]. Since the lifetime is erased, the tree could go out of scope /// while the hash map still lives. Although this could lead to lookup panics, -/// it is not unsafe since the pointers are never dereferenced. +/// it is safe since the pointers are never dereferenced. pub type ExprMap = HashMap<*const Expr, Value>; /// A reference-counted dynamic template node that can implement custom diff --git a/src/font.rs b/src/font.rs index e756f84ec..313dcf8f0 100644 --- a/src/font.rs +++ b/src/font.rs @@ -173,7 +173,7 @@ pub struct LineMetrics { impl Face { /// Parse a font face from a buffer and collection index. pub fn new(buffer: Rc>, index: u32) -> Option { - // SAFETY: + // Safety: // - The slices's location is stable in memory: // - We don't move the underlying vector // - Nobody else can move it since we have a strong ref to the `Rc`. diff --git a/src/library/utility.rs b/src/library/utility.rs index 3c157ea1c..22bde3a1a 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -23,8 +23,8 @@ pub fn len(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult { 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), + Value::Array(v) => Value::Int(v.len()), + Value::Dict(v) => Value::Int(v.len()), _ => bail!(args.source, span, "expected string, array or dictionary"), }) } diff --git a/src/parse/scanner.rs b/src/parse/scanner.rs index bb8272559..f893c4d3e 100644 --- a/src/parse/scanner.rs +++ b/src/parse/scanner.rs @@ -128,9 +128,10 @@ impl<'s> Scanner<'s> { /// The remaining source string after the current index. #[inline] pub fn rest(&self) -> &'s str { - // SAFETY: The index is always in bounds and on a codepoint boundary - // since it is: - // - either increased by the length of a scanned character, + // Safety: The index is always in bounds and on a codepoint boundary + // since it starts at zero and is is: + // - either increased by the length of a scanned character, advacing + // from one codepoint boundary to the next, // - or checked upon jumping. unsafe { self.src.get_unchecked(self.index ..) } } @@ -138,7 +139,7 @@ impl<'s> Scanner<'s> { /// The full source string up to the current index. #[inline] pub fn eaten(&self) -> &'s str { - // SAFETY: The index is always okay, for details see `rest()`. + // Safety: The index is always okay, for details see `rest()`. unsafe { self.src.get_unchecked(.. self.index) } } diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 9fd13ecc1..acd939e83 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -595,9 +595,9 @@ mod tests { // Test with each applicable suffix. for &(block, mode, suffix, token) in SUFFIXES { let src = $src; - #[allow(unused)] - let mut blocks = BLOCKS; - $(blocks = $blocks;)? + #[allow(unused_variables)] + let blocks = BLOCKS; + $(let blocks = $blocks;)? assert!(!blocks.contains(|c| !BLOCKS.contains(c))); if (mode.is_none() || mode == Some($mode)) && blocks.contains(block) { t!(@$mode: format!("{}{}", src, suffix) => $($token,)* token); diff --git a/src/pretty.rs b/src/pretty.rs index ceee61f81..bd388b567 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -554,18 +554,6 @@ impl Pretty for FuncArg { } } -impl Pretty for i64 { - fn pretty(&self, p: &mut Printer) { - write!(p, "{}", self).unwrap(); - } -} - -impl Pretty for f64 { - fn pretty(&self, p: &mut Printer) { - write!(p, "{}", self).unwrap(); - } -} - impl Pretty for str { fn pretty(&self, p: &mut Printer) { p.push('"'); @@ -594,6 +582,8 @@ macro_rules! pretty_display { } pretty_display! { + i64, + f64, bool, Length, Angle, diff --git a/src/syntax/visit.rs b/src/syntax/visit.rs index c38e87aad..81cba5c78 100644 --- a/src/syntax/visit.rs +++ b/src/syntax/visit.rs @@ -68,7 +68,6 @@ macro_rules! impl_visitor { pub mod $module { use super::*; $( - #[allow(unused_variables)] pub fn $name<'ast, V>($v: &mut V, $node: &'ast $($fmut)? $ty) where V: $visit<'ast> + ?Sized diff --git a/src/util/eco.rs b/src/util/eco.rs index 00f878723..2e326f322 100644 --- a/src/util/eco.rs +++ b/src/util/eco.rs @@ -2,11 +2,14 @@ use std::borrow::Borrow; use std::cmp::Ordering; +use std::convert::TryFrom; use std::fmt::{self, Debug, Display, Formatter, Write}; use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; use std::rc::Rc; +use crate::diag::StrResult; + /// An economical string with inline storage and clone-on-write value semantics. #[derive(Clone)] pub struct EcoString(Repr); @@ -143,21 +146,25 @@ impl EcoString { } /// Repeat this string `n` times. - pub fn repeat(&self, n: usize) -> Self { + pub fn repeat(&self, n: i64) -> StrResult { + let (n, new) = usize::try_from(n) + .ok() + .and_then(|n| self.len().checked_mul(n).map(|new| (n, new))) + .ok_or_else(|| format!("cannot repeat this string {} times", n))?; + if let Repr::Small { buf, len } = &self.0 { let prev = usize::from(*len); - let new = prev.saturating_mul(n); if new <= LIMIT { let src = &buf[.. prev]; let mut buf = [0; LIMIT]; for i in 0 .. n { buf[prev * i .. prev * (i + 1)].copy_from_slice(src); } - return Self(Repr::Small { buf, len: new as u8 }); + return Ok(Self(Repr::Small { buf, len: new as u8 })); } } - self.as_str().repeat(n).into() + Ok(self.as_str().repeat(n).into()) } } @@ -216,6 +223,13 @@ impl Deref for EcoString { fn deref(&self) -> &str { match &self.0 { + // Safety: + // The buffer contents stem from correct UTF-8 sources: + // - Valid ASCII characters + // - Other string slices + // - Chars that were encoded with char::encode_utf8 + // Furthermore, we still do the bounds-check on the len in case + // it gets corrupted somehow. Repr::Small { buf, len } => unsafe { std::str::from_utf8_unchecked(&buf[.. usize::from(*len)]) }, @@ -424,13 +438,17 @@ mod tests { #[test] fn test_str_repeat() { // Test with empty string. - assert_eq!(EcoString::new().repeat(0), ""); - assert_eq!(EcoString::new().repeat(100), ""); + assert_eq!(EcoString::new().repeat(0).unwrap(), ""); + assert_eq!(EcoString::new().repeat(100).unwrap(), ""); // Test non-spilling and spilling case. let v = EcoString::from("abc"); - assert_eq!(v.repeat(0), ""); - assert_eq!(v.repeat(3), "abcabcabc"); - assert_eq!(v.repeat(5), "abcabcabcabcabc"); + assert_eq!(v.repeat(0).unwrap(), ""); + assert_eq!(v.repeat(3).unwrap(), "abcabcabc"); + assert_eq!(v.repeat(5).unwrap(), "abcabcabcabcabc"); + assert_eq!( + v.repeat(-1).unwrap_err(), + "cannot repeat this string -1 times", + ); } } diff --git a/src/util/mod.rs b/src/util/mod.rs index dc400af83..309c1241d 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -4,6 +4,7 @@ mod eco; pub use eco::EcoString; +use std::cell::RefMut; use std::cmp::Ordering; use std::ops::Range; use std::path::{Component, Path, PathBuf}; @@ -153,3 +154,23 @@ impl PathExt for Path { out } } + +/// Additional methods for [`RefMut`]. +pub trait RefMutExt<'a, T> { + fn try_map(orig: Self, f: F) -> Result, E> + where + F: FnOnce(&mut T) -> Result<&mut U, E>; +} + +impl<'a, T> RefMutExt<'a, T> for RefMut<'a, T> { + fn try_map(mut orig: Self, f: F) -> Result, E> + where + F: FnOnce(&mut T) -> Result<&mut U, E>, + { + // Taken from here: + // https://github.com/rust-lang/rust/issues/27746#issuecomment-172899746 + f(&mut orig) + .map(|new| new as *mut U) + .map(|raw| RefMut::map(orig, |_| unsafe { &mut *raw })) + } +} diff --git a/tests/typ/code/array.typ b/tests/typ/code/array.typ index fc8795c23..df37dd454 100644 --- a/tests/typ/code/array.typ +++ b/tests/typ/code/array.typ @@ -1,6 +1,9 @@ // Test arrays. +// Ref: false --- +// Ref: true + // Empty. {()} @@ -18,6 +21,39 @@ , rgb("002") ,)} +--- +// Test lvalue and rvalue access. +{ + let array = (1, 2) + array(1) += 5 + array(0) + test(array, (1, 8)) +} + +--- +// Test rvalue out of bounds. +{ + let array = (1, 2, 3) + // Error: 3-11 array index out of bounds (index: 5, len: 3) + array(5) +} + +--- +// Test lvalue out of bounds. +{ + let array = (1, 2, 3) + // Error: 3-12 array index out of bounds (index: -1, len: 3) + array(-1) = 5 +} + +--- +// Test non-collection indexing. + +{ + let x = 10pt + // Error: 3-4 expected collection, found length + x() = 1 +} + --- // Error: 3 expected closing paren {(} diff --git a/tests/typ/code/call.typ b/tests/typ/code/call.typ index 118382e4a..92ac17ae9 100644 --- a/tests/typ/code/call.typ +++ b/tests/typ/code/call.typ @@ -68,25 +68,25 @@ C } --- -// Error: 2-6 expected function, found boolean +// Error: 2-6 expected function or collection, found boolean {true()} --- #let x = "x" -// Error: 1-3 expected function, found string +// Error: 1-3 expected function or collection, found string #x() --- #let f(x) = x -// Error: 1-6 expected function, found integer +// Error: 1-6 expected function or collection, found integer #f(1)(2) --- #let f(x) = x -// Error: 1-6 expected function, found template +// Error: 1-6 expected function or collection, found template #f[1](2) --- diff --git a/tests/typ/code/dict.typ b/tests/typ/code/dict.typ index b775f4afe..b369b8b65 100644 --- a/tests/typ/code/dict.typ +++ b/tests/typ/code/dict.typ @@ -1,12 +1,40 @@ // Test dictionaries. +// Ref: false --- +// Ref: true + // Empty {(:)} // Two pairs. {(a1: 1, a2: 2)} +--- +// Test lvalue and rvalue access. +{ + let dict = (a: 1, b: 1) + dict("b") += 1 + dict("c") = 3 + test(dict, (a: 1, b: 2, c: 3)) +} + +--- +// Test rvalue missing key. +{ + let dict = (a: 1, b: 2) + // Error: 11-20 dictionary does not contain key: "c" + let x = dict("c") +} + +--- +// Missing lvalue is automatically none-initialized. +{ + let dict = (:) + // Error: 3-17 cannot add none and integer + dict("b") += 1 +} + --- // Simple expression after already being identified as a dictionary. // Error: 9-10 expected named pair, found expression diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ index 5e56ff98c..e1662ddd5 100644 --- a/tests/typ/code/ops-invalid.typ +++ b/tests/typ/code/ops-invalid.typ @@ -46,6 +46,20 @@ // Error: 2-10 cannot divide integer by length {3 / 12pt} +--- +// Error: 2-9 cannot repeat this string -1 times +{-1 * ""} + +--- +{ + let x = 2 + for _ in 0..60 { + x *= 2 + } + // Error: 4-18 cannot repeat this string 4611686018427387904 times + {x * "abcdefgh"} +} + --- // Error: 14-22 cannot add integer and string { let x = 1; x += "2" } @@ -63,11 +77,11 @@ { 1 .. "" } --- -// Error: 3-6 cannot assign to this expression +// Error: 3-6 cannot access this expression mutably { (x) = "" } --- -// Error: 3-8 cannot assign to this expression +// Error: 3-8 cannot access this expression mutably { 1 + 2 += 3 } --- @@ -75,7 +89,7 @@ { z = 1 } --- -// Error: 3-7 cannot assign to a constant +// Error: 3-7 cannot mutate a constant { rect = "hi" } --- diff --git a/tests/typ/code/ops-prec.typ b/tests/typ/code/ops-prec.typ index e64e583cc..2cec0d046 100644 --- a/tests/typ/code/ops-prec.typ +++ b/tests/typ/code/ops-prec.typ @@ -13,7 +13,7 @@ #test(not "b" == "b", false) // Assignment binds stronger than boolean operations. -// Error: 2-7 cannot assign to this expression +// Error: 2-7 cannot access this expression mutably {not x = "a"} --- diff --git a/tests/typeset.rs b/tests/typeset.rs index a08ece2d0..650780fda 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -70,13 +70,11 @@ fn main() { let lhs = args.expect::("left-hand side")?; let rhs = args.expect::("right-hand side")?; if lhs != rhs { - typst::bail!( + return Err(Error::boxed( args.source, args.span, - "Assertion failed: {:?} != {:?}", - lhs, - rhs - ); + format!("Assertion failed: {:?} != {:?}", lhs, rhs), + )); } Ok(Value::None) });