diff --git a/src/color.rs b/src/color.rs index 9dc31c301..f349fdb47 100644 --- a/src/color.rs +++ b/src/color.rs @@ -111,7 +111,7 @@ mod tests { use super::*; #[test] - fn parse_color_strings() { + fn test_parse_color_strings() { fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); } @@ -124,7 +124,7 @@ mod tests { } #[test] - fn parse_invalid_colors() { + fn test_parse_invalid_colors() { fn test(hex: &str) { assert_eq!(RgbaColor::from_str(hex), Err(ParseRgbaError)); } diff --git a/src/eval/args.rs b/src/eval/args.rs index 43c30dafc..ea248ec42 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -1,184 +1,138 @@ -//! Simplifies argument parsing. +use super::*; -use std::mem; +/// Evaluated arguments to a function. +#[derive(Debug)] +pub struct Args { + span: Span, + pos: SpanVec, + named: Vec<(Spanned, Spanned)>, +} -use super::{Conv, EvalContext, RefKey, TryFromValue, Value, ValueDict}; -use crate::diag::Diag; -use crate::syntax::{Span, SpanVec, Spanned, WithSpan}; +impl Eval for Spanned<&Arguments> { + type Output = Args; -/// A wrapper around a dictionary value that simplifies argument parsing in -/// functions. -pub struct Args(pub Spanned); + fn eval(self, ctx: &mut EvalContext) -> Self::Output { + let mut pos = vec![]; + let mut named = vec![]; + + for arg in self.v { + match arg { + Argument::Pos(expr) => { + pos.push(expr.as_ref().eval(ctx).with_span(expr.span)); + } + Argument::Named(Named { name, expr }) => { + named.push(( + name.as_ref().map(|id| id.0.clone()), + expr.as_ref().eval(ctx).with_span(expr.span), + )); + } + } + } + + Args { span: self.span, pos, named } + } +} impl Args { - /// Retrieve and remove the argument associated with the given key if there - /// is any. + /// Find the first convertible positional argument. + pub fn find(&mut self, ctx: &mut EvalContext) -> Option + where + T: Cast>, + { + self.pos.iter_mut().find_map(move |slot| try_cast(ctx, slot)) + } + + /// Find the first convertible positional argument, producing an error if + /// no match was found. + pub fn require(&mut self, ctx: &mut EvalContext, what: &str) -> Option + where + T: Cast>, + { + let found = self.find(ctx); + if found.is_none() { + ctx.diag(error!(self.span, "missing argument: {}", what)); + } + found + } + + /// Filter out all convertible positional arguments. + pub fn filter<'a, T>( + &'a mut self, + ctx: &'a mut EvalContext, + ) -> impl Iterator + 'a + where + T: Cast>, + { + self.pos.iter_mut().filter_map(move |slot| try_cast(ctx, slot)) + } + + /// Convert the value for the given named argument. /// - /// Generates an error if the key exists, but the value can't be converted - /// into the type `T`. - pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option + /// Generates an error if the conversion fails. + pub fn get<'a, T>(&mut self, ctx: &mut EvalContext, name: &str) -> Option where - K: Into>, - T: TryFromValue, + T: Cast>, { - self.0.v.remove(key).and_then(|entry| { - let span = entry.value.span; - conv_diag( - T::try_from_value(entry.value), - &mut ctx.feedback.diags, - span, - ) - }) + let index = self.named.iter().position(|(k, _)| k.v.as_str() == name)?; + let value = self.named.remove(index).1; + cast(ctx, value) } - /// This is the same as [`get`](Self::get), except that it generates an error about a - /// missing argument with the given `name` if the key does not exist. - pub fn need<'a, K, T>( - &mut self, - ctx: &mut EvalContext, - key: K, - name: &str, - ) -> Option - where - K: Into>, - T: TryFromValue, - { - if let Some(entry) = self.0.v.remove(key) { - let span = entry.value.span; - conv_diag( - T::try_from_value(entry.value), - &mut ctx.feedback.diags, - span, - ) - } else { - ctx.diag(error!(self.0.span, "missing argument: {}", name)); - None - } - } - - /// Retrieve and remove the first matching positional argument. - pub fn find(&mut self) -> Option - where - T: TryFromValue, - { - for (&key, entry) in self.0.v.nums_mut() { - let span = entry.value.span; - let slot = &mut entry.value; - let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span); - if let Some(t) = conv { - self.0.v.remove(key); - return Some(t); + /// Generate "unexpected argument" errors for all remaining arguments. + pub fn finish(self, ctx: &mut EvalContext) { + let a = self.pos.iter().map(|v| v.as_ref()); + let b = self.named.iter().map(|(k, v)| (&v.v).with_span(k.span.join(v.span))); + for value in a.chain(b) { + if value.v != &Value::Error { + ctx.diag(error!(value.span, "unexpected argument")); } } - None - } - - /// Retrieve and remove all matching positional arguments. - pub fn find_all(&mut self) -> impl Iterator + '_ - where - T: TryFromValue, - { - let mut skip = 0; - std::iter::from_fn(move || { - for (&key, entry) in self.0.v.nums_mut().skip(skip) { - let span = entry.value.span; - let slot = &mut entry.value; - let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span); - if let Some(t) = conv { - self.0.v.remove(key); - return Some(t); - } - skip += 1; - } - None - }) - } - - /// Retrieve and remove all matching named arguments. - pub fn find_all_str(&mut self) -> impl Iterator + '_ - where - T: TryFromValue, - { - let mut skip = 0; - std::iter::from_fn(move || { - for (key, entry) in self.0.v.strs_mut().skip(skip) { - let span = entry.value.span; - let slot = &mut entry.value; - let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span); - if let Some(t) = conv { - let key = key.clone(); - self.0.v.remove(&key); - return Some((key, t)); - } - skip += 1; - } - - None - }) - } - - /// Generated _unexpected argument_ errors for all remaining entries. - pub fn done(&self, ctx: &mut EvalContext) { - for entry in self.0.v.values() { - let span = entry.key_span.join(entry.value.span); - ctx.diag(error!(span, "unexpected argument")); - } } } -fn conv_diag(conv: Conv, diags: &mut SpanVec, span: Span) -> Option { - match conv { - Conv::Ok(t) => Some(t), - Conv::Warn(t, warn) => { - diags.push(warn.with_span(span)); +/// Cast the value into `T`, generating an error if the conversion fails. +fn cast(ctx: &mut EvalContext, value: Spanned) -> Option +where + T: Cast>, +{ + let span = value.span; + match T::cast(value) { + CastResult::Ok(t) => Some(t), + CastResult::Warn(t, m) => { + ctx.diag(warning!(span, "{}", m)); Some(t) } - Conv::Err(_, err) => { - diags.push(err.with_span(span)); + CastResult::Err(value) => { + ctx.diag(error!( + span, + "expected {}, found {}", + T::TYPE_NAME, + value.v.type_name() + )); None } } } -fn conv_put_back(conv: Conv, slot: &mut Spanned, span: Span) -> Option { - match conv { - Conv::Ok(t) => Some(t), - Conv::Warn(t, _) => Some(t), - Conv::Err(v, _) => { - *slot = v.with_span(span); +/// Try to cast the value in the slot into `T`, putting it back if the +/// conversion fails. +fn try_cast(ctx: &mut EvalContext, slot: &mut Spanned) -> Option +where + T: Cast>, +{ + // Replace with error placeholder when conversion works since error values + // are ignored when generating "unexpected argument" errors. + let value = std::mem::replace(slot, Spanned::zero(Value::Error)); + let span = value.span; + match T::cast(value) { + CastResult::Ok(t) => Some(t), + CastResult::Warn(t, m) => { + ctx.diag(warning!(span, "{}", m)); + Some(t) + } + CastResult::Err(value) => { + *slot = value; None } } } - -#[cfg(test)] -mod tests { - use super::super::{Dict, SpannedEntry, Value}; - use super::*; - - fn entry(value: Value) -> SpannedEntry { - SpannedEntry::value(Spanned::zero(value)) - } - - #[test] - fn test_args_find() { - let mut args = Args(Spanned::zero(Dict::new())); - args.0.v.insert(1, entry(Value::Bool(false))); - args.0.v.insert(2, entry(Value::Str("hi".to_string()))); - assert_eq!(args.find::(), Some("hi".to_string())); - assert_eq!(args.0.v.len(), 1); - assert_eq!(args.find::(), Some(false)); - assert!(args.0.v.is_empty()); - } - - #[test] - fn test_args_find_all() { - let mut args = Args(Spanned::zero(Dict::new())); - args.0.v.insert(1, entry(Value::Bool(false))); - args.0.v.insert(3, entry(Value::Float(0.0))); - args.0.v.insert(7, entry(Value::Bool(true))); - assert_eq!(args.find_all::().collect::>(), [false, true]); - assert_eq!(args.0.v.len(), 1); - assert_eq!(args.0.v[3].value.v, Value::Float(0.0)); - } -} diff --git a/src/eval/dict.rs b/src/eval/dict.rs deleted file mode 100644 index efa5cb373..000000000 --- a/src/eval/dict.rs +++ /dev/null @@ -1,522 +0,0 @@ -//! A key-value map that can also model array-like structures. - -use std::collections::BTreeMap; -use std::fmt::{self, Debug, Display, Formatter}; -use std::iter::{Extend, FromIterator}; -use std::ops::Index; - -use crate::syntax::{Span, Spanned}; - -/// A dictionary data structure, which maps from integers and strings to a -/// generic value type. -/// -/// The dictionary can be used to model arrays by assigning values to successive -/// indices from `0..n`. The `push` method offers special support for this -/// pattern. -#[derive(Clone)] -pub struct Dict { - nums: BTreeMap, - strs: BTreeMap, - lowest_free: u64, -} - -impl Dict { - /// Create a new empty dictionary. - pub fn new() -> Self { - Self { - nums: BTreeMap::new(), - strs: BTreeMap::new(), - lowest_free: 0, - } - } - - /// The total number of entries in the dictionary. - pub fn len(&self) -> usize { - self.nums.len() + self.strs.len() - } - - /// Whether the dictionary contains no entries. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// The first number key-value pair (with lowest number). - pub fn first(&self) -> Option<(u64, &V)> { - self.nums.iter().next().map(|(&k, v)| (k, v)) - } - - /// The last number key-value pair (with highest number). - pub fn last(&self) -> Option<(u64, &V)> { - self.nums.iter().next_back().map(|(&k, v)| (k, v)) - } - - /// Get a reference to the value with the given key. - pub fn get<'a, K>(&self, key: K) -> Option<&V> - where - K: Into>, - { - match key.into() { - RefKey::Num(num) => self.nums.get(&num), - RefKey::Str(string) => self.strs.get(string), - } - } - - /// Borrow the value with the given key mutably. - pub fn get_mut<'a, K>(&mut self, key: K) -> Option<&mut V> - where - K: Into>, - { - match key.into() { - RefKey::Num(num) => self.nums.get_mut(&num), - RefKey::Str(string) => self.strs.get_mut(string), - } - } - - /// Insert a value into the dictionary. - pub fn insert(&mut self, key: K, value: V) - where - K: Into, - { - match key.into() { - DictKey::Num(num) => { - self.nums.insert(num, value); - if self.lowest_free == num { - self.lowest_free += 1; - } - } - DictKey::Str(string) => { - self.strs.insert(string, value); - } - } - } - - /// Remove the value with the given key from the dictionary. - pub fn remove<'a, K>(&mut self, key: K) -> Option - where - K: Into>, - { - match key.into() { - RefKey::Num(num) => { - self.lowest_free = self.lowest_free.min(num); - self.nums.remove(&num) - } - RefKey::Str(string) => self.strs.remove(string), - } - } - - /// Append a value to the dictionary. - /// - /// This will associate the `value` with the lowest free number key (zero if - /// there is no number key so far). - pub fn push(&mut self, value: V) { - while self.nums.contains_key(&self.lowest_free) { - self.lowest_free += 1; - } - self.nums.insert(self.lowest_free, value); - self.lowest_free += 1; - } -} - -impl<'a, K, V> Index for Dict -where - K: Into>, -{ - type Output = V; - - fn index(&self, index: K) -> &Self::Output { - self.get(index).expect("key not in dict") - } -} - -impl Eq for Dict {} - -impl PartialEq for Dict { - fn eq(&self, other: &Self) -> bool { - self.iter().eq(other.iter()) - } -} - -impl Default for Dict { - fn default() -> Self { - Self::new() - } -} - -impl Debug for Dict { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if self.is_empty() { - return f.write_str("()"); - } - - let mut builder = f.debug_tuple(""); - - struct Entry<'a>(bool, &'a dyn Display, &'a dyn Debug); - impl<'a> Debug for Entry<'a> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if self.0 { - f.write_str("\"")?; - } - self.1.fmt(f)?; - if self.0 { - f.write_str("\"")?; - } - - f.write_str(": ")?; - - self.2.fmt(f) - } - } - - for (key, value) in self.nums() { - builder.field(&Entry(false, &key, &value)); - } - - for (key, value) in self.strs() { - builder.field(&Entry(key.contains(' '), &key, &value)); - } - - builder.finish() - } -} - -/// Iteration. -impl Dict { - /// Iterator over all borrowed keys and values. - pub fn iter(&self) -> impl Iterator { - self.into_iter() - } - - /// Iterator over all borrowed keys and values. - pub fn iter_mut(&mut self) -> impl Iterator { - self.into_iter() - } - - /// Iterate over all values in the dictionary. - pub fn values(&self) -> impl Iterator { - self.nums().map(|(_, v)| v).chain(self.strs().map(|(_, v)| v)) - } - - /// Iterate over all values in the dictionary. - pub fn values_mut(&mut self) -> impl Iterator { - self.nums - .iter_mut() - .map(|(_, v)| v) - .chain(self.strs.iter_mut().map(|(_, v)| v)) - } - - /// Move into an owned iterator over all values in the dictionary. - pub fn into_values(self) -> impl Iterator { - self.nums - .into_iter() - .map(|(_, v)| v) - .chain(self.strs.into_iter().map(|(_, v)| v)) - } - - /// Iterate over the number key-value pairs. - pub fn nums(&self) -> std::collections::btree_map::Iter { - self.nums.iter() - } - - /// Iterate mutably over the number key-value pairs. - pub fn nums_mut(&mut self) -> std::collections::btree_map::IterMut { - self.nums.iter_mut() - } - - /// Iterate over the number key-value pairs. - pub fn into_nums(self) -> std::collections::btree_map::IntoIter { - self.nums.into_iter() - } - - /// Iterate over the string key-value pairs. - pub fn strs(&self) -> std::collections::btree_map::Iter { - self.strs.iter() - } - - /// Iterate mutably over the string key-value pairs. - pub fn strs_mut(&mut self) -> std::collections::btree_map::IterMut { - self.strs.iter_mut() - } - - /// Iterate over the string key-value pairs. - pub fn into_strs(self) -> std::collections::btree_map::IntoIter { - self.strs.into_iter() - } -} - -impl Extend<(DictKey, V)> for Dict { - fn extend>(&mut self, iter: T) { - for (key, value) in iter.into_iter() { - self.insert(key, value); - } - } -} - -impl FromIterator<(DictKey, V)> for Dict { - fn from_iter>(iter: T) -> Self { - let mut v = Self::new(); - v.extend(iter); - v - } -} - -impl IntoIterator for Dict { - type Item = (DictKey, V); - type IntoIter = std::iter::Chain< - std::iter::Map< - std::collections::btree_map::IntoIter, - fn((u64, V)) -> (DictKey, V), - >, - std::iter::Map< - std::collections::btree_map::IntoIter, - fn((String, V)) -> (DictKey, V), - >, - >; - - fn into_iter(self) -> Self::IntoIter { - let nums = self.nums.into_iter().map((|(k, v)| (DictKey::Num(k), v)) as _); - let strs = self.strs.into_iter().map((|(k, v)| (DictKey::Str(k), v)) as _); - nums.chain(strs) - } -} - -impl<'a, V> IntoIterator for &'a Dict { - type Item = (RefKey<'a>, &'a V); - type IntoIter = std::iter::Chain< - std::iter::Map< - std::collections::btree_map::Iter<'a, u64, V>, - fn((&'a u64, &'a V)) -> (RefKey<'a>, &'a V), - >, - std::iter::Map< - std::collections::btree_map::Iter<'a, String, V>, - fn((&'a String, &'a V)) -> (RefKey<'a>, &'a V), - >, - >; - - fn into_iter(self) -> Self::IntoIter { - let nums = self.nums().map((|(k, v): (&u64, _)| (RefKey::Num(*k), v)) as _); - let strs = self.strs().map((|(k, v): (&'a String, _)| (RefKey::Str(k), v)) as _); - nums.chain(strs) - } -} - -impl<'a, V> IntoIterator for &'a mut Dict { - type Item = (RefKey<'a>, &'a mut V); - type IntoIter = std::iter::Chain< - std::iter::Map< - std::collections::btree_map::IterMut<'a, u64, V>, - fn((&'a u64, &'a mut V)) -> (RefKey<'a>, &'a mut V), - >, - std::iter::Map< - std::collections::btree_map::IterMut<'a, String, V>, - fn((&'a String, &'a mut V)) -> (RefKey<'a>, &'a mut V), - >, - >; - - fn into_iter(self) -> Self::IntoIter { - let nums = self - .nums - .iter_mut() - .map((|(k, v): (&u64, _)| (RefKey::Num(*k), v)) as _); - let strs = self - .strs - .iter_mut() - .map((|(k, v): (&'a String, _)| (RefKey::Str(k), v)) as _); - nums.chain(strs) - } -} - -/// The owned variant of a dictionary key. -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum DictKey { - Num(u64), - Str(String), -} - -impl From<&Self> for DictKey { - fn from(key: &Self) -> Self { - key.clone() - } -} - -impl From> for DictKey { - fn from(key: RefKey<'_>) -> Self { - match key { - RefKey::Num(num) => Self::Num(num), - RefKey::Str(string) => Self::Str(string.to_string()), - } - } -} - -impl From for DictKey { - fn from(num: u64) -> Self { - Self::Num(num) - } -} - -impl From for DictKey { - fn from(string: String) -> Self { - Self::Str(string) - } -} - -impl From<&'static str> for DictKey { - fn from(string: &'static str) -> Self { - Self::Str(string.to_string()) - } -} - -/// The borrowed variant of a dictionary key. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum RefKey<'a> { - Num(u64), - Str(&'a str), -} - -impl From for RefKey<'static> { - fn from(num: u64) -> Self { - Self::Num(num) - } -} - -impl<'a> From<&'a String> for RefKey<'a> { - fn from(string: &'a String) -> Self { - Self::Str(&string) - } -} - -impl<'a> From<&'a str> for RefKey<'a> { - fn from(string: &'a str) -> Self { - Self::Str(string) - } -} - -/// A dictionary entry which combines key span and value. -/// -/// This exists because a key in a directory can't track its span by itself. -#[derive(Clone, PartialEq)] -pub struct SpannedEntry { - pub key_span: Span, - pub value: Spanned, -} - -impl SpannedEntry { - /// Create a new entry. - pub fn new(key: Span, val: Spanned) -> Self { - Self { key_span: key, value: val } - } - - /// Create an entry with the same span for key and value. - pub fn value(val: Spanned) -> Self { - Self { key_span: val.span, value: val } - } - - /// Convert from `&SpannedEntry` to `SpannedEntry<&T>` - pub fn as_ref(&self) -> SpannedEntry<&V> { - SpannedEntry { - key_span: self.key_span, - value: self.value.as_ref(), - } - } - - /// Map the entry to a different value type. - pub fn map(self, f: impl FnOnce(V) -> U) -> SpannedEntry { - SpannedEntry { - key_span: self.key_span, - value: self.value.map(f), - } - } -} - -impl Debug for SpannedEntry { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - f.write_str("key")?; - self.key_span.fmt(f)?; - f.write_str(" ")?; - } - self.value.fmt(f) - } -} - -#[cfg(test)] -mod tests { - use super::Dict; - - #[test] - fn test_dict_different_key_types_dont_interfere() { - let mut dict = Dict::new(); - dict.insert(10, "hello"); - dict.insert("twenty", "there"); - assert_eq!(dict.len(), 2); - assert_eq!(dict[10], "hello"); - assert_eq!(dict["twenty"], "there"); - } - - #[test] - fn test_dict_push_skips_already_inserted_keys() { - let mut dict = Dict::new(); - dict.insert(2, "2"); - dict.push("0"); - dict.insert(3, "3"); - dict.push("1"); - dict.push("4"); - assert_eq!(dict.len(), 5); - assert_eq!(dict[0], "0"); - assert_eq!(dict[1], "1"); - assert_eq!(dict[2], "2"); - assert_eq!(dict[3], "3"); - assert_eq!(dict[4], "4"); - } - - #[test] - fn test_dict_push_remove_push_reuses_index() { - let mut dict = Dict::new(); - dict.push("0"); - dict.push("1"); - dict.push("2"); - dict.remove(1); - dict.push("a"); - dict.push("3"); - assert_eq!(dict.len(), 4); - assert_eq!(dict[0], "0"); - assert_eq!(dict[1], "a"); - assert_eq!(dict[2], "2"); - assert_eq!(dict[3], "3"); - } - - #[test] - fn test_dict_first_and_last_are_correct() { - let mut dict = Dict::new(); - assert_eq!(dict.first(), None); - assert_eq!(dict.last(), None); - dict.insert(4, "hi"); - dict.insert("string", "hi"); - assert_eq!(dict.first(), Some((4, &"hi"))); - assert_eq!(dict.last(), Some((4, &"hi"))); - dict.insert(2, "bye"); - assert_eq!(dict.first(), Some((2, &"bye"))); - assert_eq!(dict.last(), Some((4, &"hi"))); - } - - #[test] - fn test_dict_format_debug() { - let mut dict = Dict::new(); - assert_eq!(format!("{:?}", dict), "()"); - assert_eq!(format!("{:#?}", dict), "()"); - - dict.insert(10, "hello"); - dict.insert("twenty", "there"); - dict.insert("sp ace", "quotes"); - assert_eq!( - format!("{:?}", dict), - r#"(10: "hello", "sp ace": "quotes", twenty: "there")"#, - ); - assert_eq!(format!("{:#?}", dict).lines().collect::>(), [ - "(", - r#" 10: "hello","#, - r#" "sp ace": "quotes","#, - r#" twenty: "there","#, - ")", - ]); - } -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 56946210c..62bd444c5 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -3,12 +3,10 @@ #[macro_use] mod value; mod args; -mod dict; mod scope; mod state; pub use args::*; -pub use dict::*; pub use scope::*; pub use state::*; pub use value::*; @@ -451,7 +449,13 @@ impl Eval for Spanned<&Lit> { fn eval(self, ctx: &mut EvalContext) -> Self::Output { match *self.v { - Lit::Ident(ref v) => Value::Ident(v.clone()), + Lit::Ident(ref v) => match ctx.state.scope.get(v.as_str()) { + Some(value) => value.clone(), + None => { + ctx.diag(error!(self.span, "unknown variable")); + Value::Error + } + }, Lit::Bool(v) => Value::Bool(v), Lit::Int(v) => Value::Int(v), Lit::Float(v) => Value::Float(v), @@ -459,28 +463,29 @@ impl Eval for Spanned<&Lit> { Lit::Percent(v) => Value::Relative(Relative::new(v / 100.0)), Lit::Color(v) => Value::Color(Color::Rgba(v)), Lit::Str(ref v) => Value::Str(v.clone()), + Lit::Array(ref v) => Value::Array(v.with_span(self.span).eval(ctx)), Lit::Dict(ref v) => Value::Dict(v.with_span(self.span).eval(ctx)), Lit::Content(ref v) => Value::Content(v.clone()), } } } -impl Eval for Spanned<&LitDict> { + +impl Eval for Spanned<&Array> { + type Output = ValueArray; + + fn eval(self, ctx: &mut EvalContext) -> Self::Output { + self.v.iter().map(|expr| expr.as_ref().eval(ctx)).collect() + } +} + +impl Eval for Spanned<&Dict> { type Output = ValueDict; fn eval(self, ctx: &mut EvalContext) -> Self::Output { - let mut dict = ValueDict::new(); - - for entry in &self.v.0 { - let val = entry.expr.as_ref().eval(ctx); - let spanned = val.with_span(entry.expr.span); - if let Some(key) = &entry.key { - dict.insert(&key.v, SpannedEntry::new(key.span, spanned)); - } else { - dict.push(SpannedEntry::value(spanned)); - } - } - - dict + self.v + .iter() + .map(|Named { name, expr }| (name.v.0.clone(), expr.as_ref().eval(ctx))) + .collect() } } @@ -490,19 +495,27 @@ impl Eval for Spanned<&ExprCall> { fn eval(self, ctx: &mut EvalContext) -> Self::Output { let name = &self.v.name.v; let span = self.v.name.span; - let dict = self.v.args.as_ref().eval(ctx); - if let Some(func) = ctx.state.scope.get(name) { - let args = Args(dict.with_span(self.v.args.span)); - ctx.feedback.decos.push(Deco::Resolved.with_span(span)); - (func.clone())(args, ctx) - } else { - if !name.is_empty() { - ctx.diag(error!(span, "unknown function")); - ctx.feedback.decos.push(Deco::Unresolved.with_span(span)); + if let Some(value) = ctx.state.scope.get(name) { + if let Value::Func(func) = value { + let func = func.clone(); + ctx.feedback.decos.push(Deco::Resolved.with_span(span)); + + let mut args = self.v.args.as_ref().eval(ctx); + let returned = func(ctx, &mut args); + args.finish(ctx); + + return returned; + } else { + let ty = value.type_name(); + ctx.diag(error!(span, "a value of type {} is not callable", ty)); } - Value::Dict(dict) + } else if !name.is_empty() { + ctx.diag(error!(span, "unknown function")); } + + ctx.feedback.decos.push(Deco::Unresolved.with_span(span)); + Value::Error } } @@ -554,7 +567,7 @@ fn neg(ctx: &mut EvalContext, span: Span, value: Value) -> Value { Relative(v) => Relative(-v), Linear(v) => Linear(-v), v => { - ctx.diag(error!(span, "cannot negate {}", v.ty())); + ctx.diag(error!(span, "cannot negate {}", v.type_name())); Value::Error } } @@ -589,7 +602,12 @@ fn add(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { (Content(a), Content(b)) => Content(concat(a, b)), (a, b) => { - ctx.diag(error!(span, "cannot add {} and {}", a.ty(), b.ty())); + ctx.diag(error!( + span, + "cannot add {} and {}", + a.type_name(), + b.type_name() + )); Value::Error } } @@ -617,7 +635,12 @@ fn sub(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { (Linear(a), Linear(b)) => Linear(a - b), (a, b) => { - ctx.diag(error!(span, "cannot subtract {1} from {0}", a.ty(), b.ty())); + ctx.diag(error!( + span, + "cannot subtract {1} from {0}", + a.type_name(), + b.type_name() + )); Value::Error } } @@ -652,7 +675,12 @@ fn mul(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { (Str(a), Int(b)) => Str(a.repeat(0.max(b) as usize)), (a, b) => { - ctx.diag(error!(span, "cannot multiply {} with {}", a.ty(), b.ty())); + ctx.diag(error!( + span, + "cannot multiply {} with {}", + a.type_name(), + b.type_name() + )); Value::Error } } @@ -677,7 +705,12 @@ fn div(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value { (Linear(a), Float(b)) => Linear(a / b), (a, b) => { - ctx.diag(error!(span, "cannot divide {} by {}", a.ty(), b.ty())); + ctx.diag(error!( + span, + "cannot divide {} by {}", + a.type_name(), + b.type_name() + )); Value::Error } } diff --git a/src/eval/scope.rs b/src/eval/scope.rs index c06243937..c9ce1423e 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -3,12 +3,12 @@ use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; -use super::value::ValueFunc; +use super::Value; /// A map from identifiers to functions. #[derive(Default, Clone, PartialEq)] pub struct Scope { - functions: HashMap, + values: HashMap, } impl Scope { @@ -18,19 +18,19 @@ impl Scope { Self::default() } - /// Return the function with the given name if there is one. - pub fn get(&self, name: &str) -> Option<&ValueFunc> { - self.functions.get(name) + /// Return the value of the given variable. + pub fn get(&self, var: &str) -> Option<&Value> { + self.values.get(var) } - /// Associate the given name with the function. - pub fn set(&mut self, name: impl Into, function: ValueFunc) { - self.functions.insert(name.into(), function); + /// Store the value for the given variable. + pub fn set(&mut self, var: impl Into, value: impl Into) { + self.values.insert(var.into(), value.into()); } } impl Debug for Scope { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.functions.keys()).finish() + self.values.fmt(f) } } diff --git a/src/eval/value.rs b/src/eval/value.rs index a009e8911..d1dcdcfa2 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -1,25 +1,21 @@ //! Computational values. +use std::any::Any; +use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::ops::Deref; use std::rc::Rc; -use fontdock::{FontStretch, FontStyle, FontWeight}; - -use super::{Args, Dict, Eval, EvalContext, SpannedEntry}; +use super::{Args, Eval, EvalContext}; use crate::color::Color; -use crate::diag::Diag; -use crate::geom::{Dir, Length, Linear, Relative}; -use crate::paper::Paper; -use crate::syntax::{Ident, Spanned, SynTree, WithSpan}; +use crate::geom::{Length, Linear, Relative}; +use crate::syntax::{Spanned, SynTree, WithSpan}; /// A computational value. #[derive(Clone, PartialEq)] pub enum Value { /// The value that indicates the absence of a meaningful value. None, - /// An identifier: `ident`. - Ident(Ident), /// A boolean: `true, false`. Bool(bool), /// An integer: `120`. @@ -36,34 +32,46 @@ pub enum Value { Color(Color), /// A string: `"string"`. Str(String), - /// A dictionary value: `(false, 12cm, greeting: "hi")`. + /// An array value: `(1, "hi", 12cm)`. + Array(ValueArray), + /// A dictionary value: `(color: #f79143, pattern: dashed)`. Dict(ValueDict), /// A content value: `{*Hi* there}`. - Content(SynTree), + Content(ValueContent), /// An executable function. Func(ValueFunc), + /// Any object. + Any(ValueAny), /// The result of invalid operations. Error, } impl Value { - /// The natural-language name of this value's type for use in error - /// messages. - pub fn ty(&self) -> &'static str { + /// Try to cast the value into a specific type. + pub fn cast(self) -> CastResult + where + T: Cast, + { + T::cast(self) + } + + /// The name of the stored value's type. + pub fn type_name(&self) -> &'static str { match self { Self::None => "none", - Self::Ident(_) => "identifier", - Self::Bool(_) => "bool", - Self::Int(_) => "integer", - Self::Float(_) => "float", - Self::Relative(_) => "relative", - Self::Length(_) => "length", - Self::Linear(_) => "linear", - Self::Color(_) => "color", - Self::Str(_) => "string", - Self::Dict(_) => "dict", - Self::Content(_) => "content", - Self::Func(_) => "function", + Self::Bool(_) => bool::TYPE_NAME, + Self::Int(_) => i64::TYPE_NAME, + Self::Float(_) => f64::TYPE_NAME, + Self::Relative(_) => Relative::TYPE_NAME, + Self::Length(_) => Length::TYPE_NAME, + Self::Linear(_) => Linear::TYPE_NAME, + Self::Color(_) => Color::TYPE_NAME, + Self::Str(_) => String::TYPE_NAME, + Self::Array(_) => ValueArray::TYPE_NAME, + Self::Dict(_) => ValueDict::TYPE_NAME, + Self::Content(_) => ValueContent::TYPE_NAME, + Self::Func(_) => ValueFunc::TYPE_NAME, + Self::Any(v) => v.type_name(), Self::Error => "error", } } @@ -81,13 +89,6 @@ impl Eval for &Value { // Pass through. Value::Content(tree) => tree.eval(ctx), - // Forward to each dictionary entry. - Value::Dict(dict) => { - for entry in dict.values() { - entry.value.v.eval(ctx); - } - } - // Format with debug. val => ctx.push(ctx.make_text_node(format!("{:?}", val))), } @@ -104,7 +105,6 @@ impl Debug for Value { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::None => f.pad("none"), - Self::Ident(v) => v.fmt(f), Self::Bool(v) => v.fmt(f), Self::Int(v) => v.fmt(f), Self::Float(v) => v.fmt(f), @@ -113,45 +113,36 @@ impl Debug for Value { Self::Linear(v) => v.fmt(f), Self::Color(v) => v.fmt(f), Self::Str(v) => v.fmt(f), + Self::Array(v) => v.fmt(f), Self::Dict(v) => v.fmt(f), Self::Content(v) => v.fmt(f), Self::Func(v) => v.fmt(f), + Self::Any(v) => v.fmt(f), Self::Error => f.pad(""), } } } -/// A dictionary of values. -/// -/// # Example -/// ```typst -/// (false, 12cm, greeting: "hi") -/// ``` -pub type ValueDict = Dict>; +/// An array value: `(1, "hi", 12cm)`. +pub type ValueArray = Vec; -/// An wrapper around a reference-counted function trait object. -/// -/// The dynamic function object is wrapped in an `Rc` to keep [`Value`] -/// cloneable. -/// -/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for -/// [`Value`] when directly putting the `Rc` in there, see the [Rust -/// Issue]. -/// -/// [Rust Issue]: https://github.com/rust-lang/rust/issues/31740 +/// A dictionary value: `(color: #f79143, pattern: dashed)`. +pub type ValueDict = HashMap; + +/// A content value: `{*Hi* there}`. +pub type ValueContent = SynTree; + +/// A wrapper around a reference-counted executable function. #[derive(Clone)] -pub struct ValueFunc(pub Rc); - -/// The signature of executable functions. -type Func = dyn Fn(Args, &mut EvalContext) -> Value; +pub struct ValueFunc(Rc Value>); impl ValueFunc { /// Create a new function value from a rust function or closure. - pub fn new(f: F) -> Self + pub fn new(func: F) -> Self where - F: Fn(Args, &mut EvalContext) -> Value + 'static, + F: Fn(&mut EvalContext, &mut Args) -> Value + 'static, { - Self(Rc::new(f)) + Self(Rc::new(func)) } } @@ -162,7 +153,7 @@ impl PartialEq for ValueFunc { } impl Deref for ValueFunc { - type Target = Func; + type Target = dyn Fn(&mut EvalContext, &mut Args) -> Value; fn deref(&self) -> &Self::Target { self.0.as_ref() @@ -175,160 +166,288 @@ impl Debug for ValueFunc { } } -/// Try to convert a value into a more specific type. -pub trait TryFromValue: Sized { - /// Try to convert the value into yourself. - fn try_from_value(value: Spanned) -> Conv; +/// A wrapper around a dynamic value. +pub struct ValueAny(Box); + +impl ValueAny { + /// Create a new instance from any value that satisifies the required bounds. + pub fn new(any: T) -> Self + where + T: Type + Debug + Clone + PartialEq + 'static, + { + Self(Box::new(any)) + } + + /// Whether the wrapped type is `T`. + pub fn is(&self) -> bool { + self.0.as_any().is::() + } + + /// Try to downcast to a specific type. + pub fn downcast(self) -> Result { + if self.is::() { + Ok(*self.0.into_any().downcast().unwrap()) + } else { + Err(self) + } + } + + /// Try to downcast to a reference to a specific type. + pub fn downcast_ref(&self) -> Option<&T> { + self.0.as_any().downcast_ref() + } + + /// The name of the stored object's type. + pub fn type_name(&self) -> &'static str { + self.0.dyn_type_name() + } } -/// The result of a conversion. -#[derive(Debug, Clone, PartialEq)] -pub enum Conv { - /// Success conversion. +impl Clone for ValueAny { + fn clone(&self) -> Self { + Self(self.0.dyn_clone()) + } +} + +impl PartialEq for ValueAny { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other) + } +} + +impl Debug for ValueAny { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +trait Bounds: Debug + 'static { + fn as_any(&self) -> &dyn Any; + fn into_any(self: Box) -> Box; + fn dyn_eq(&self, other: &ValueAny) -> bool; + fn dyn_clone(&self) -> Box; + fn dyn_type_name(&self) -> &'static str; +} + +impl Bounds for T +where + T: Type + Debug + Clone + PartialEq + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn into_any(self: Box) -> Box { + self + } + + fn dyn_eq(&self, other: &ValueAny) -> bool { + if let Some(other) = other.downcast_ref::() { + self == other + } else { + false + } + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn dyn_type_name(&self) -> &'static str { + T::TYPE_NAME + } +} + +/// Types that can be stored in values. +pub trait Type { + /// The name of the type. + const TYPE_NAME: &'static str; +} + +impl Type for Spanned +where + T: Type, +{ + const TYPE_NAME: &'static str = T::TYPE_NAME; +} + +/// Cast from a value to a specific type. +pub trait Cast: Type + Sized { + /// Try to cast the value into an instance of `Self`. + fn cast(value: V) -> CastResult; +} + +/// The result of casting a value to a specific type. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum CastResult { + /// The value was cast successfully. Ok(T), - /// Sucessful conversion with a warning. - Warn(T, Diag), - /// Unsucessful conversion, gives back the value alongside the error. - Err(Value, Diag), + /// The value was cast successfully, but with a warning message. + Warn(T, String), + /// The value could not be cast into the specified type. + Err(V), } -impl Conv { - /// Map the conversion result. - pub fn map(self, f: impl FnOnce(T) -> U) -> Conv { +impl CastResult { + /// Access the conversion resulting, discarding a possibly existing warning. + pub fn ok(self) -> Option { match self { - Conv::Ok(t) => Conv::Ok(f(t)), - Conv::Warn(t, warn) => Conv::Warn(f(t), warn), - Conv::Err(v, err) => Conv::Err(v, err), + CastResult::Ok(t) | CastResult::Warn(t, _) => Some(t), + CastResult::Err(_) => None, } } } -impl TryFromValue for Spanned { - fn try_from_value(value: Spanned) -> Conv { +impl Cast> for T +where + T: Cast, +{ + fn cast(value: Spanned) -> CastResult> { let span = value.span; - T::try_from_value(value).map(|v| v.with_span(span)) + match T::cast(value.v) { + CastResult::Ok(t) => CastResult::Ok(t), + CastResult::Warn(t, m) => CastResult::Warn(t, m), + CastResult::Err(v) => CastResult::Err(v.with_span(span)), + } } } -/// A value type that matches [identifier](Value::Ident) and [string](Value::Str) values. -pub struct StringLike(pub String); - -impl From for String { - fn from(like: StringLike) -> String { - like.0 +impl Cast> for Spanned +where + T: Cast, +{ + fn cast(value: Spanned) -> CastResult> { + let span = value.span; + match T::cast(value.v) { + CastResult::Ok(t) => CastResult::Ok(t.with_span(span)), + CastResult::Warn(t, m) => CastResult::Warn(t.with_span(span), m), + CastResult::Err(v) => CastResult::Err(v.with_span(span)), + } } } -impl Deref for StringLike { - type Target = str; +macro_rules! impl_primitive { + ($type:ty: + $type_name:literal, + $variant:path + $(, $pattern:pat => $out:expr)* $(,)? + ) => { + impl Type for $type { + const TYPE_NAME: &'static str = $type_name; + } - fn deref(&self) -> &str { - self.0.as_str() - } -} + impl From<$type> for Value { + fn from(v: $type) -> Self { + $variant(v) + } + } -/// Implement [`TryFromValue`] through a match. -macro_rules! try_from_match { - ($type:ty[$name:literal] $(@ $span:ident)?: $($pattern:pat => $output:expr),* $(,)?) => { - impl $crate::eval::TryFromValue for $type { - fn try_from_value(value: Spanned) -> $crate::eval::Conv { - use $crate::eval::Conv; - #[allow(unused)] - $(let $span = value.span;)? - #[allow(unreachable_patterns)] - match value.v { - $($pattern => Conv::Ok($output)),*, - v => { - let e = error!("expected {}, found {}", $name, v.ty()); - Conv::Err(v, e) - } + impl Cast for $type { + fn cast(value: Value) -> CastResult { + match value { + $variant(v) => CastResult::Ok(v), + $($pattern => CastResult::Ok($out),)* + v => CastResult::Err(v), } } } }; } -/// Implement [`TryFromValue`] through a function parsing an identifier. -macro_rules! try_from_id { - ($type:ty[$name:literal]: $from_str:expr) => { - impl $crate::eval::TryFromValue for $type { - fn try_from_value(value: Spanned) -> $crate::eval::Conv { - use $crate::eval::Conv; - let v = value.v; - if let Value::Ident(id) = v { - if let Some(v) = $from_str(&id) { - Conv::Ok(v) - } else { - Conv::Err(Value::Ident(id), error!("invalid {}", $name)) - } - } else { - let e = error!("expected identifier, found {}", v.ty()); - Conv::Err(v, e) - } - } - } - }; -} +impl_primitive! { bool: "boolean", Value::Bool } +impl_primitive! { i64: "integer", Value::Int } +impl_primitive! { Length: "length", Value::Length } +impl_primitive! { Relative: "relative", Value::Relative } +impl_primitive! { Color: "color", Value::Color } +impl_primitive! { String: "string", Value::Str } +impl_primitive! { ValueArray: "array", Value::Array } +impl_primitive! { ValueDict: "dictionary", Value::Dict } +impl_primitive! { ValueContent: "content", Value::Content } +impl_primitive! { ValueFunc: "function", Value::Func } -try_from_match!(Value["value"]: v => v); -try_from_match!(Ident["identifier"]: Value::Ident(v) => v); -try_from_match!(bool["bool"]: Value::Bool(v) => v); -try_from_match!(i64["integer"]: Value::Int(v) => v); -try_from_match!(f64["float"]: +impl_primitive! { + f64: "float", + Value::Float, Value::Int(v) => v as f64, - Value::Float(v) => v, -); -try_from_match!(Length["length"]: Value::Length(v) => v); -try_from_match!(Relative["relative"]: Value::Relative(v) => v); -try_from_match!(Linear["linear"]: - Value::Linear(v) => v, +} + +impl_primitive! { + Linear: "linear", + Value::Linear, Value::Length(v) => v.into(), Value::Relative(v) => v.into(), -); -try_from_match!(Color["color"]: Value::Color(v) => v); -try_from_match!(String["string"]: Value::Str(v) => v); -try_from_match!(SynTree["tree"]: Value::Content(v) => v); -try_from_match!(ValueDict["dictionary"]: Value::Dict(v) => v); -try_from_match!(ValueFunc["function"]: Value::Func(v) => v); -try_from_match!(StringLike["identifier or string"]: - Value::Ident(Ident(v)) => Self(v), - Value::Str(v) => Self(v), -); -try_from_id!(Dir["direction"]: |v| match v { - "ltr" | "left-to-right" => Some(Self::LTR), - "rtl" | "right-to-left" => Some(Self::RTL), - "ttb" | "top-to-bottom" => Some(Self::TTB), - "btt" | "bottom-to-top" => Some(Self::BTT), - _ => None, -}); -try_from_id!(FontStyle["font style"]: Self::from_str); -try_from_id!(FontStretch["font stretch"]: Self::from_str); -try_from_id!(Paper["paper"]: Self::from_name); +} -impl TryFromValue for FontWeight { - fn try_from_value(value: Spanned) -> Conv { - match value.v { - Value::Int(number) => { - let [min, max] = [Self::THIN, Self::BLACK]; - if number < i64::from(min.to_number()) { - Conv::Warn(min, warning!("the minimum font weight is {:#?}", min)) - } else if number > i64::from(max.to_number()) { - Conv::Warn(max, warning!("the maximum font weight is {:#?}", max)) - } else { - Conv::Ok(Self::from_number(number as u16)) - } - } - Value::Ident(id) => { - if let Some(weight) = Self::from_str(&id) { - Conv::Ok(weight) - } else { - Conv::Err(Value::Ident(id), error!("invalid font weight")) - } - } - v => { - let e = error!("expected font weight, found {}", v.ty()); - Conv::Err(v, e) - } - } +impl From<&str> for Value { + fn from(v: &str) -> Self { + Self::Str(v.to_string()) } } + +impl From for Value +where + F: Fn(&mut EvalContext, &mut Args) -> Value + 'static, +{ + fn from(func: F) -> Self { + Self::Func(ValueFunc::new(func)) + } +} + +impl From for Value { + fn from(v: ValueAny) -> Self { + Self::Any(v) + } +} + +/// Make a type usable with [`ValueAny`]. +/// +/// Given a type `T`, this implements the following traits: +/// - [`Type`] for `T`, +/// - [`From`](From) for [`Value`], +/// - [`Cast`](Cast) for `T`. +#[macro_export] +macro_rules! impl_type { + ($type:ty: + $type_name:literal + $(, $pattern:pat => $out:expr)* + $(, #($anyvar:ident: $anytype:ty) => $anyout:expr)* + $(,)? + ) => { + impl $crate::eval::Type for $type { + const TYPE_NAME: &'static str = $type_name; + } + + impl From<$type> for $crate::eval::Value { + fn from(any: $type) -> Self { + $crate::eval::Value::Any($crate::eval::ValueAny::new(any)) + } + } + + impl $crate::eval::Cast<$crate::eval::Value> for $type { + fn cast( + value: $crate::eval::Value, + ) -> $crate::eval::CastResult { + use $crate::eval::*; + + #[allow(unreachable_code)] + match value { + $($pattern => CastResult::Ok($out),)* + Value::Any(mut any) => { + any = match any.downcast::() { + Ok(t) => return CastResult::Ok(t), + Err(any) => any, + }; + + $(any = match any.downcast::<$anytype>() { + Ok($anyvar) => return CastResult::Ok($anyout), + Err(any) => any, + };)* + + CastResult::Err(Value::Any(any)) + }, + v => CastResult::Err(v), + } + } + } + }; +} diff --git a/src/geom/length.rs b/src/geom/length.rs index 061510a13..0e4451530 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -208,14 +208,14 @@ mod tests { use super::*; #[test] - fn test_length_formats_correctly() { + fn test_length_unit_conversion() { + assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4); + } + + #[test] + fn test_length_formatting() { assert_eq!(Length::pt(-28.34).to_string(), "-1.00cm".to_string()); assert_eq!(Length::pt(23.0).to_string(), "23.00pt".to_string()); assert_eq!(Length::cm(12.728).to_string(), "12.73cm".to_string()); } - - #[test] - fn test_length_unit_conversion() { - assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4); - } } diff --git a/src/library/insert.rs b/src/library/insert.rs index 6f267b7af..587f96dc7 100644 --- a/src/library/insert.rs +++ b/src/library/insert.rs @@ -6,14 +6,14 @@ use crate::prelude::*; /// `image`: Insert an image. /// -/// # Positional arguments -/// - Path (`string`): The path to the image file. -/// /// Supports PNG and JPEG files. -pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value { - let path = args.need::<_, Spanned>(ctx, 0, "path"); - let width = args.get::<_, Linear>(ctx, "width"); - let height = args.get::<_, Linear>(ctx, "height"); +/// +/// # Positional arguments +/// - Path to image file: of type `string`. +pub fn image(ctx: &mut EvalContext, args: &mut Args) -> Value { + let path = args.require::>(ctx, "path to image file"); + let width = args.get(ctx, "width"); + let height = args.get(ctx, "height"); if let Some(path) = path { let mut env = ctx.env.borrow_mut(); diff --git a/src/library/layout.rs b/src/library/layout.rs index c888ea185..ac152d075 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -1,51 +1,44 @@ -use std::fmt::{self, Display, Formatter}; - use crate::eval::Softness; -use crate::geom::{Length, Linear}; use crate::layout::{Expansion, Fixed, Spacing, Stack}; use crate::paper::{Paper, PaperClass}; use crate::prelude::*; /// `align`: Align content along the layouting axes. /// +/// Which axis an alignment should apply to (main or cross) is inferred from +/// either the argument itself (for anything other than `center`) or from the +/// second argument if present, defaulting to the cross axis for a single +/// `center` alignment. +/// /// # Positional arguments -/// - first (optional, `Alignment`): An alignment for any of the two axes. -/// - second (optional, `Alignment`): An alignment for the other axis. +/// - Alignments: variadic, of type `alignment`. /// /// # Named arguments -/// - `horizontal` (`Alignment`): An alignment for the horizontal axis. -/// - `vertical` (`Alignment`): An alignment for the vertical axis. +/// - Horizontal alignment: `horizontal`, of type `alignment`. +/// - Vertical alignment: `vertical`, of type `alignment`. /// -/// # Enumerations -/// - `Alignment` +/// # Relevant types and constants +/// - Type `alignment` /// - `left` /// - `right` /// - `top` /// - `bottom` /// - `center` -/// -/// # Notes -/// Which axis an alignment should apply to (main or cross) is inferred from -/// either the argument itself (for anything other than `center`) or from the -/// second argument if present, defaulting to the cross axis for a single -/// `center` alignment. -pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { +pub fn align(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); - let body = args.find::(); - let first = args.get::<_, Spanned>(ctx, 0); - let second = args.get::<_, Spanned>(ctx, 1); - let hor = args.get::<_, Spanned>(ctx, "horizontal"); - let ver = args.get::<_, Spanned>(ctx, "vertical"); - args.done(ctx); - let prev_main = ctx.state.align.main; + let first = args.find(ctx); + let second = args.find(ctx); + let hor = args.get(ctx, "horizontal"); + let ver = args.get(ctx, "vertical"); + let mut had = Gen::uniform(false); let mut had_center = false; for (axis, Spanned { v: arg, span }) in first .into_iter() .chain(second.into_iter()) - .map(|arg| (arg.v.axis(), arg)) + .map(|arg: Spanned| (arg.v.axis(), arg)) .chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg))) .chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg))) { @@ -56,10 +49,7 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { let gen_align = arg.switch(ctx.state.flow); if arg.axis().map_or(false, |a| a != axis) { - ctx.diag(error!( - span, - "invalid alignment `{}` for {} axis", arg, axis, - )); + ctx.diag(error!(span, "invalid alignment for {} axis", axis)); } else if had.get(gen_axis) { ctx.diag(error!(span, "duplicate alignment for {} axis", axis)); } else { @@ -69,7 +59,7 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { } else { // We don't know the axis: This has to be a `center` alignment for a // positional argument. - debug_assert_eq!(arg, SpecAlign::Center); + debug_assert_eq!(arg, Alignment::Center); if had.main && had.cross { ctx.diag(error!(span, "duplicate alignment")); @@ -104,12 +94,12 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { ctx.state.align.cross = Align::Center; } - if ctx.state.align.main != prev_main { + if ctx.state.align.main != snapshot.align.main { ctx.end_par_group(); ctx.start_par_group(); } - if let Some(body) = body { + if let Some(body) = args.find::(ctx) { body.eval(ctx); ctx.state = snapshot; } @@ -117,30 +107,18 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value { Value::None } -/// An argument to `[align]`. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -enum SpecAlign { +pub(crate) enum Alignment { Left, + Center, Right, Top, Bottom, - Center, } -try_from_id!(SpecAlign["alignment"]: |v| match v { - "left" => Some(Self::Left), - "right" => Some(Self::Right), - "top" => Some(Self::Top), - "bottom" => Some(Self::Bottom), - "center" => Some(Self::Center), - _ => None, -}); - -impl SpecAlign { +impl Alignment { /// The specific axis this alignment refers to. - /// - /// Returns `None` if this is `Center` since the axis is unknown. - pub fn axis(self) -> Option { + fn axis(self) -> Option { match self { Self::Left => Some(SpecAxis::Horizontal), Self::Right => Some(SpecAxis::Horizontal), @@ -151,7 +129,7 @@ impl SpecAlign { } } -impl Switch for SpecAlign { +impl Switch for Alignment { type Other = Align; fn switch(self, flow: Flow) -> Self::Other { @@ -174,38 +152,34 @@ impl Switch for SpecAlign { } } -impl Display for SpecAlign { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Left => "left", - Self::Right => "right", - Self::Top => "top", - Self::Bottom => "bottom", - Self::Center => "center", - }) - } +impl_type! { + Alignment: "alignment" } /// `box`: Layout content into a box. /// /// # Named arguments -/// - `width` (`linear` relative to parent width): The width of the box. -/// - `height` (`linear` relative to parent height): The height of the box. -pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { +/// - Width of the box: `width`, of type `linear` relative to parent width. +/// - Height of the box: `height`, of type `linear` relative to parent height. +pub fn boxed(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); - let body = args.find::().unwrap_or_default(); - let width = args.get::<_, Linear>(ctx, "width"); - let height = args.get::<_, Linear>(ctx, "height"); - let main = args.get::<_, Spanned>(ctx, "main-dir"); - let cross = args.get::<_, Spanned>(ctx, "cross-dir"); + + let width = args.get(ctx, "width"); + let height = args.get(ctx, "height"); + let main = args.get(ctx, "main-dir"); + let cross = args.get(ctx, "cross-dir"); + ctx.set_flow(Gen::new(main, cross)); - args.done(ctx); let flow = ctx.state.flow; let align = ctx.state.align; ctx.start_content_group(); - body.eval(ctx); + + if let Some(body) = args.find::(ctx) { + body.eval(ctx); + } + let children = ctx.end_content_group(); ctx.push(Fixed { @@ -227,31 +201,34 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value { Value::None } +impl_type! { + Dir: "direction" +} + /// `h`: Add horizontal spacing. /// /// # Positional arguments -/// - Spacing (`linear` relative to font size): The amount of spacing. -pub fn h(args: Args, ctx: &mut EvalContext) -> Value { - spacing(args, ctx, SpecAxis::Horizontal) +/// - Amount of spacing: of type `linear` relative to current font size. +pub fn h(ctx: &mut EvalContext, args: &mut Args) -> Value { + spacing(ctx, args, SpecAxis::Horizontal) } /// `v`: Add vertical spacing. /// /// # Positional arguments -/// - Spacing (`linear` relative to font size): The amount of spacing. -pub fn v(args: Args, ctx: &mut EvalContext) -> Value { - spacing(args, ctx, SpecAxis::Vertical) +/// - Amount of spacing: of type `linear` relative to current font size. +pub fn v(ctx: &mut EvalContext, args: &mut Args) -> Value { + spacing(ctx, args, SpecAxis::Vertical) } /// Apply spacing along a specific axis. -fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value { - let spacing = args.need::<_, Linear>(ctx, 0, "spacing"); - args.done(ctx); +fn spacing(ctx: &mut EvalContext, args: &mut Args, axis: SpecAxis) -> Value { + let spacing: Option = args.require(ctx, "spacing"); if let Some(linear) = spacing { let amount = linear.resolve(ctx.state.font.font_size()); let spacing = Spacing { amount, softness: Softness::Hard }; - if ctx.state.flow.main.axis() == axis { + if axis == ctx.state.flow.main.axis() { ctx.end_par_group(); ctx.push(spacing); ctx.start_par_group(); @@ -266,79 +243,78 @@ fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value { /// `page`: Configure pages. /// /// # Positional arguments -/// - Paper name (optional, `Paper`). +/// - Paper name: optional, of type `string`, see [here](crate::paper) for a +/// full list of all paper names. /// /// # Named arguments -/// - `width` (`length`): The width of pages. -/// - `height` (`length`): The height of pages. -/// - `margins` (`linear` relative to sides): The margins for all sides. -/// - `left` (`linear` relative to width): The left margin. -/// - `right` (`linear` relative to width): The right margin. -/// - `top` (`linear` relative to height): The top margin. -/// - `bottom` (`linear` relative to height): The bottom margin. -/// - `flip` (`bool`): Flips custom or paper-defined width and height. -/// -/// # Enumerations -/// - `Paper`: See [here](crate::paper) for a full list. -pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value { +/// - Width of the page: `width`, of type `length`. +/// - Height of the page: `height`, of type `length`. +/// - Margins for all sides: `margins`, of type `linear` relative to sides. +/// - Left margin: `left`, of type `linear` relative to width. +/// - Right margin: `right`, of type `linear` relative to width. +/// - Top margin: `top`, of type `linear` relative to height. +/// - Bottom margin: `bottom`, of type `linear` relative to height. +/// - Flip width and height: `flip`, of type `bool`. +pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); - let body = args.find::(); - if let Some(paper) = args.get::<_, Paper>(ctx, 0) { - ctx.state.page.class = paper.class; - ctx.state.page.size = paper.size(); + if let Some(name) = args.find::>(ctx) { + if let Some(paper) = Paper::from_name(&name.v) { + ctx.state.page.class = paper.class; + ctx.state.page.size = paper.size(); + } else { + ctx.diag(error!(name.span, "invalid paper name")); + } } - if let Some(width) = args.get::<_, Length>(ctx, "width") { + if let Some(width) = args.get(ctx, "width") { ctx.state.page.class = PaperClass::Custom; ctx.state.page.size.width = width; } - if let Some(height) = args.get::<_, Length>(ctx, "height") { + if let Some(height) = args.get(ctx, "height") { ctx.state.page.class = PaperClass::Custom; ctx.state.page.size.height = height; } - if let Some(margins) = args.get::<_, Linear>(ctx, "margins") { + if let Some(margins) = args.get(ctx, "margins") { ctx.state.page.margins = Sides::uniform(Some(margins)); } - if let Some(left) = args.get::<_, Linear>(ctx, "left") { + if let Some(left) = args.get(ctx, "left") { ctx.state.page.margins.left = Some(left); } - if let Some(top) = args.get::<_, Linear>(ctx, "top") { + if let Some(top) = args.get(ctx, "top") { ctx.state.page.margins.top = Some(top); } - if let Some(right) = args.get::<_, Linear>(ctx, "right") { + if let Some(right) = args.get(ctx, "right") { ctx.state.page.margins.right = Some(right); } - if let Some(bottom) = args.get::<_, Linear>(ctx, "bottom") { + if let Some(bottom) = args.get(ctx, "bottom") { ctx.state.page.margins.bottom = Some(bottom); } - if args.get::<_, bool>(ctx, "flip").unwrap_or(false) { + if args.get(ctx, "flip").unwrap_or(false) { let size = &mut ctx.state.page.size; std::mem::swap(&mut size.width, &mut size.height); } - let main = args.get::<_, Spanned>(ctx, "main-dir"); - let cross = args.get::<_, Spanned>(ctx, "cross-dir"); + let main = args.get(ctx, "main-dir"); + let cross = args.get(ctx, "cross-dir"); + ctx.set_flow(Gen::new(main, cross)); - args.done(ctx); - let mut softness = ctx.end_page_group(|_| false); - - if let Some(body) = body { + if let Some(body) = args.find::(ctx) { // TODO: Restrict body to a single page? ctx.start_page_group(Softness::Hard); body.eval(ctx); ctx.end_page_group(|s| s == Softness::Hard); - ctx.state = snapshot; softness = Softness::Soft; + ctx.state = snapshot; } ctx.start_page_group(softness); @@ -347,8 +323,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value { } /// `pagebreak`: Start a new page. -pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value { - args.done(ctx); +pub fn pagebreak(ctx: &mut EvalContext, _: &mut Args) -> Value { ctx.end_page_group(|_| true); ctx.start_page_group(Softness::Hard); Value::None diff --git a/src/library/mod.rs b/src/library/mod.rs index 806b02759..1cc7b9e95 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -8,31 +8,60 @@ pub use insert::*; pub use layout::*; pub use style::*; -use crate::eval::{Scope, ValueFunc}; +use fontdock::{FontStretch, FontStyle, FontWeight}; -macro_rules! std { - ($($func:expr $(=> $name:expr)?),* $(,)?) => { - /// The scope containing all standard library functions. - pub fn _std() -> Scope { - let mut std = Scope::new(); - $( - let _name = stringify!($func); - $(let _name = $name;)? - std.set(_name, ValueFunc::new($func)); - )* - std - } - }; -} +use crate::eval::Scope; +use crate::geom::Dir; -std! { - align, - boxed => "box", - font, - h, - image, - page, - pagebreak, - rgb, - v, +/// The scope containing the standard library. +pub fn _std() -> Scope { + let mut std = Scope::new(); + + // Functions. + std.set("align", align); + std.set("box", boxed); + std.set("font", font); + std.set("h", h); + std.set("image", image); + std.set("page", page); + std.set("pagebreak", pagebreak); + std.set("rgb", rgb); + std.set("v", v); + + // Constants. + std.set("left", Alignment::Left); + std.set("center", Alignment::Center); + std.set("right", Alignment::Right); + std.set("top", Alignment::Top); + std.set("bottom", Alignment::Bottom); + std.set("ltr", Dir::LTR); + std.set("rtl", Dir::RTL); + std.set("ttb", Dir::TTB); + std.set("btt", Dir::BTT); + std.set("serif", FontFamily::Serif); + std.set("sans-serif", FontFamily::SansSerif); + std.set("monospace", FontFamily::Monospace); + std.set("normal", FontStyle::Normal); + std.set("italic", FontStyle::Italic); + std.set("oblique", FontStyle::Oblique); + std.set("thin", FontWeight::THIN); + std.set("extralight", FontWeight::EXTRALIGHT); + std.set("light", FontWeight::LIGHT); + std.set("regular", FontWeight::REGULAR); + std.set("medium", FontWeight::MEDIUM); + std.set("semibold", FontWeight::SEMIBOLD); + std.set("bold", FontWeight::BOLD); + std.set("extrabold", FontWeight::EXTRABOLD); + std.set("black", FontWeight::BLACK); + std.set("ultra-condensed", FontStretch::UltraCondensed); + std.set("extra-condensed", FontStretch::ExtraCondensed); + std.set("condensed", FontStretch::Condensed); + std.set("semi-condensed", FontStretch::SemiCondensed); + std.set("normal", FontStretch::Normal); + std.set("semi-expanded", FontStretch::SemiExpanded); + std.set("expanded", FontStretch::Expanded); + std.set("extra-expanded", FontStretch::ExtraExpanded); + std.set("ultra-expanded", FontStretch::UltraExpanded); + + std } diff --git a/src/library/style.rs b/src/library/style.rs index 768380464..3bdcdfd4b 100644 --- a/src/library/style.rs +++ b/src/library/style.rs @@ -1,58 +1,41 @@ +use std::fmt::{self, Display, Formatter}; use std::rc::Rc; use fontdock::{FontStretch, FontStyle, FontWeight}; use crate::color::{Color, RgbaColor}; -use crate::eval::StringLike; -use crate::geom::Linear; use crate::prelude::*; /// `font`: Configure the font. /// /// # Positional arguments -/// - Font size (optional, `linear` relative to current font size). -/// - Font families ... (optional, variadic, `Family`) +/// - Font size: optional, of type `linear` relative to current font size. +/// - Font families: variadic, of type `font-family`. /// /// # Named arguments -/// - `style` (`Style`): The font style. -/// - `weight` (`Weight`): The font weight. -/// - `stretch` (`Stretch`): The font stretch. -/// - `serif` (`Family` or `dict` of type `Family`): The serif family. -/// - `sans-serif` (`Family` or `dict` of type `Family`): The new sansserif family. -/// - `monospace` (`Family` or `dict` of type `Family`): The monospace family. -/// - `emoji` (`Family` or `dict` of type `Family`): The emoji family. -/// - `math` (`Family` or `dict` of type `Family`): The math family. +/// - Font Style: `style`, of type `font-style`. +/// - Font Weight: `weight`, of type `font-weight`. +/// - Font Stretch: `stretch`, of type `font-stretch`. +/// - Serif family definition: `serif`, of type `font-families`. +/// - Sans-serif family definition: `sans-serif`, of type `font-families`. +/// - Monospace family definition: `monospace`, of type `font-families`. /// -/// # Examples -/// Set font size and font families. -/// ```typst -/// [font 12pt, "Arial", "Noto Sans", sans-serif] -/// ``` -/// -/// Redefine the default sans-serif family to a single font family. -/// ```typst -/// [font sans-serif: "Source Sans Pro"] -/// ``` -/// -/// Redefine the default emoji family with a fallback. -/// ```typst -/// [font emoji: ("Segoe UI Emoji", "Noto Emoji")] -/// ``` -/// -/// # Enumerations -/// - `Family` +/// # Relevant types and constants +/// - Type `font-families` +/// - coerces from `string` +/// - coerces from `array` +/// - coerces from `font-family` +/// - Type `font-family` /// - `serif` /// - `sans-serif` /// - `monospace` -/// - `emoji` -/// - `math` -/// - any string -/// - `Style` +/// - coerces from `string` +/// - Type `font-style` /// - `normal` /// - `italic` /// - `oblique` -/// - `Weight` -/// - `thin` or `hairline` (100) +/// - Type `font-weight` +/// - `thin` (100) /// - `extralight` (200) /// - `light` (300) /// - `regular` (400) @@ -61,8 +44,8 @@ use crate::prelude::*; /// - `bold` (700) /// - `extrabold` (800) /// - `black` (900) -/// - any integer between 100 and 900 -/// - `Stretch` +/// - coerces from `integer` +/// - Type `font-stretch` /// - `ultra-condensed` /// - `extra-condensed` /// - `condensed` @@ -72,11 +55,10 @@ use crate::prelude::*; /// - `expanded` /// - `extra-expanded` /// - `ultra-expanded` -pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { +pub fn font(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); - let body = args.find::(); - if let Some(linear) = args.find::() { + if let Some(linear) = args.find::(ctx) { if linear.is_absolute() { ctx.state.font.size = linear.abs; ctx.state.font.scale = Relative::ONE.into(); @@ -85,52 +67,35 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { } } - let mut needs_flattening = false; - let list: Vec<_> = args.find_all::().map(|s| s.to_lowercase()).collect(); - + let list: Vec<_> = args.filter::(ctx).map(|f| f.to_string()).collect(); if !list.is_empty() { - Rc::make_mut(&mut ctx.state.font.families).list = list; - needs_flattening = true; + let families = Rc::make_mut(&mut ctx.state.font.families); + families.list = list; + families.flatten(); } - if let Some(style) = args.get::<_, FontStyle>(ctx, "style") { + if let Some(style) = args.get(ctx, "style") { ctx.state.font.variant.style = style; } - if let Some(weight) = args.get::<_, FontWeight>(ctx, "weight") { + if let Some(weight) = args.get(ctx, "weight") { ctx.state.font.variant.weight = weight; } - if let Some(stretch) = args.get::<_, FontStretch>(ctx, "stretch") { + if let Some(stretch) = args.get(ctx, "stretch") { ctx.state.font.variant.stretch = stretch; } - struct FamilyList(Vec); - - try_from_match!(FamilyList["family or list of families"] @ span: - Value::Str(v) => Self(vec![v.to_lowercase()]), - Value::Dict(v) => Self(Args(v.with_span(span)) - .find_all::() - .map(|s| s.to_lowercase()) - .collect() - ), - ); - - for &class in &["serif", "sans-serif", "monospace", "emoji", "math"] { - if let Some(list) = args.get::<_, FamilyList>(ctx, class) { - Rc::make_mut(&mut ctx.state.font.families) - .update_class_list(class.to_string(), list.0); - needs_flattening = true; + for variant in FontFamily::VARIANTS { + if let Some(FontFamilies(list)) = args.get(ctx, variant.as_str()) { + let strings = list.into_iter().map(|f| f.to_string()).collect(); + let families = Rc::make_mut(&mut ctx.state.font.families); + families.update_class_list(variant.to_string(), strings); + families.flatten(); } } - if needs_flattening { - Rc::make_mut(&mut ctx.state.font.families).flatten(); - } - - args.done(ctx); - - if let Some(body) = body { + if let Some(body) = args.find::(ctx) { body.eval(ctx); ctx.state = snapshot; } @@ -138,24 +103,94 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value { Value::None } +/// A list of font families. +#[derive(Debug, Clone, PartialEq)] +struct FontFamilies(Vec); + +impl_type! { + FontFamilies: "font family or array of font families", + Value::Str(string) => Self(vec![FontFamily::Named(string.to_lowercase())]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect() + ), + #(family: FontFamily) => Self(vec![family]), +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) enum FontFamily { + Serif, + SansSerif, + Monospace, + Named(String), +} + +impl FontFamily { + pub const VARIANTS: &'static [Self] = + &[Self::Serif, Self::SansSerif, Self::Monospace]; + + pub fn as_str(&self) -> &str { + match self { + Self::Serif => "serif", + Self::SansSerif => "sans-serif", + Self::Monospace => "monospace", + Self::Named(s) => s, + } + } +} + +impl Display for FontFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self.as_str()) + } +} + +impl_type! { + FontFamily: "font family", + Value::Str(string) => Self::Named(string.to_lowercase()) +} + +impl_type! { + FontStyle: "font style" +} + +impl_type! { + FontWeight: "font weight", + Value::Int(number) => { + let [min, max] = [Self::THIN, Self::BLACK]; + let message = || format!("must be between {:#?} and {:#?}", min, max); + return if number < i64::from(min.to_number()) { + CastResult::Warn(min, message()) + } else if number > i64::from(max.to_number()) { + CastResult::Warn(max, message()) + } else { + CastResult::Ok(Self::from_number(number as u16)) + }; + }, +} + +impl_type! { + FontStretch: "font stretch" +} + /// `rgb`: Create an RGB(A) color. /// /// # Positional arguments -/// - Red component (`float` between 0.0 and 1.0). -/// - Green component (`float` between 0.0 and 1.0). -/// - Blue component (`float` between 0.0 and 1.0). -/// - Alpha component (optional, `float` between 0.0 and 1.0). -pub fn rgb(mut args: Args, ctx: &mut EvalContext) -> Value { - let r = args.need::<_, Spanned>(ctx, 0, "red component"); - let g = args.need::<_, Spanned>(ctx, 1, "green component"); - let b = args.need::<_, Spanned>(ctx, 2, "blue component"); - let a = args.get::<_, Spanned>(ctx, 3); - args.done(ctx); +/// - Red component: of type `float`, between 0.0 and 1.0. +/// - Green component: of type `float`, between 0.0 and 1.0. +/// - Blue component: of type `float`, between 0.0 and 1.0. +/// - Alpha component: optional, of type `float`, between 0.0 and 1.0. +pub fn rgb(ctx: &mut EvalContext, args: &mut Args) -> Value { + let r = args.require(ctx, "red component"); + let g = args.require(ctx, "green component"); + let b = args.require(ctx, "blue component"); + let a = args.find(ctx); let mut clamp = |component: Option>, default| { component.map_or(default, |c| { if c.v < 0.0 || c.v > 1.0 { - ctx.diag(error!(c.span, "should be between 0.0 and 1.0")); + ctx.diag(warning!(c.span, "must be between 0.0 and 1.0")); } (c.v.max(0.0).min(1.0) * 255.0).round() as u8 }) diff --git a/src/parse/collection.rs b/src/parse/collection.rs new file mode 100644 index 000000000..db267dbed --- /dev/null +++ b/src/parse/collection.rs @@ -0,0 +1,142 @@ +use super::*; +use crate::diag::Deco; + +/// Parse the arguments to a function call. +pub fn arguments(p: &mut Parser) -> Arguments { + collection(p, vec![]) +} + +/// Parse a parenthesized group, which can be either of: +/// - Array literal +/// - Dictionary literal +/// - Parenthesized expression +pub fn parenthesized(p: &mut Parser) -> Expr { + p.start_group(Group::Paren); + let state = if p.eat_if(Token::Colon) { + collection(p, State::Dict(vec![])) + } else { + collection(p, State::Unknown) + }; + p.end_group(); + state.into_expr() +} + +/// Parse a collection. +fn collection(p: &mut Parser, mut collection: T) -> T { + let mut missing_coma = None; + + while !p.eof() { + if let Some(arg) = p.span_if(argument) { + collection.push_arg(p, arg); + + if let Some(pos) = missing_coma.take() { + p.diag_expected_at("comma", pos); + } + + if p.eof() { + break; + } + + let behind = p.last_end(); + if p.eat_if(Token::Comma) { + collection.push_comma(); + } else { + missing_coma = Some(behind); + } + } + } + + collection +} + +/// Parse an expression or a named pair. +fn argument(p: &mut Parser) -> Option { + let first = p.span_if(expr)?; + if p.eat_if(Token::Colon) { + if let Expr::Lit(Lit::Ident(ident)) = first.v { + let expr = p.span_if(expr)?; + let name = ident.with_span(first.span); + p.deco(Deco::Name.with_span(name.span)); + Some(Argument::Named(Named { name, expr })) + } else { + p.diag(error!(first.span, "name must be identifier")); + expr(p); + None + } + } else { + Some(Argument::Pos(first)) + } +} + +/// Abstraction for comma-separated list of expression / named pairs. +trait Collection { + fn push_arg(&mut self, p: &mut Parser, arg: Spanned); + fn push_comma(&mut self) {} +} + +impl Collection for Arguments { + fn push_arg(&mut self, _: &mut Parser, arg: Spanned) { + self.push(arg.v); + } +} + +/// State of collection parsing. +#[derive(Debug)] +enum State { + Unknown, + Expr(Spanned), + Array(Array), + Dict(Dict), +} + +impl State { + fn into_expr(self) -> Expr { + match self { + Self::Unknown => Expr::Lit(Lit::Array(vec![])), + Self::Expr(expr) => expr.v, + Self::Array(array) => Expr::Lit(Lit::Array(array)), + Self::Dict(dict) => Expr::Lit(Lit::Dict(dict)), + } + } +} + +impl Collection for State { + fn push_arg(&mut self, p: &mut Parser, arg: Spanned) { + match self { + Self::Unknown => match arg.v { + Argument::Pos(expr) => *self = Self::Expr(expr), + Argument::Named(named) => *self = Self::Dict(vec![named]), + }, + Self::Expr(prev) => match arg.v { + Argument::Pos(expr) => *self = Self::Array(vec![take(prev), expr]), + Argument::Named(_) => diag(p, arg), + }, + Self::Array(array) => match arg.v { + Argument::Pos(expr) => array.push(expr), + Argument::Named(_) => diag(p, arg), + }, + Self::Dict(dict) => match arg.v { + Argument::Pos(_) => diag(p, arg), + Argument::Named(named) => dict.push(named), + }, + } + } + + fn push_comma(&mut self) { + if let Self::Expr(expr) = self { + *self = Self::Array(vec![take(expr)]); + } + } +} + +fn take(expr: &mut Spanned) -> Spanned { + // Replace with anything, it's overwritten anyway. + std::mem::replace(expr, Spanned::zero(Expr::Lit(Lit::Bool(false)))) +} + +fn diag(p: &mut Parser, arg: Spanned) { + p.diag(error!(arg.span, "{}", match arg.v { + Argument::Pos(_) => "expected named pair, found expression", + Argument::Named(_) => "expected expression, found named pair", + })); +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 7880dd7a3..912a34d07 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,5 +1,6 @@ //! Parsing and tokenization. +mod collection; mod lines; mod parser; mod resolve; @@ -15,10 +16,11 @@ pub use tokens::*; use std::str::FromStr; use crate::color::RgbaColor; -use crate::diag::{Deco, Pass}; -use crate::eval::DictKey; +use crate::diag::Pass; use crate::syntax::*; +use collection::{arguments, parenthesized}; + /// Parse a string of source code. pub fn parse(src: &str) -> Pass { let mut p = Parser::new(src); @@ -153,6 +155,9 @@ fn block_expr(p: &mut Parser) -> Option { p.push_mode(TokenMode::Header); p.start_group(Group::Brace); let expr = expr(p); + while !p.eof() { + p.diag_unexpected(); + } p.pop_mode(); p.end_group(); expr @@ -161,7 +166,7 @@ fn block_expr(p: &mut Parser) -> Option { /// Parse a parenthesized function call. fn paren_call(p: &mut Parser, name: Spanned) -> ExprCall { p.start_group(Group::Paren); - let args = p.span(|p| dict_contents(p).0); + let args = p.span(arguments); p.end_group(); ExprCall { name, args } } @@ -184,16 +189,16 @@ fn bracket_call(p: &mut Parser) -> ExprCall { p.end_group(); if p.peek() == Some(Token::LeftBracket) { - let expr = p.span(|p| Expr::Lit(Lit::Content(bracket_body(p)))); - inner.span.expand(expr.span); - inner.v.args.v.0.push(LitDictEntry { key: None, expr }); + let body = p.span(|p| Expr::Lit(Lit::Content(bracket_body(p)))); + inner.span.expand(body.span); + inner.v.args.v.push(Argument::Pos(body)); } while let Some(mut top) = outer.pop() { let span = inner.span; let node = inner.map(|c| SynNode::Expr(Expr::Call(c))); let expr = Expr::Lit(Lit::Content(vec![node])).with_span(span); - top.v.args.v.0.push(LitDictEntry { key: None, expr }); + top.v.args.v.push(Argument::Pos(expr)); inner = top; } @@ -215,9 +220,9 @@ fn bracket_subheader(p: &mut Parser) -> ExprCall { Ident(String::new()).with_span(start) }); - let args = p.span(|p| dict_contents(p).0); - + let args = p.span(arguments); p.end_group(); + ExprCall { name, args } } @@ -231,75 +236,6 @@ fn bracket_body(p: &mut Parser) -> SynTree { tree } -/// Parse the contents of a dictionary. -fn dict_contents(p: &mut Parser) -> (LitDict, bool) { - let mut dict = LitDict::new(); - let mut missing_coma = None; - let mut comma_and_keyless = true; - - while !p.eof() { - if let Some(entry) = dict_entry(p) { - let behind = entry.expr.span.end; - if let Some(pos) = missing_coma.take() { - p.diag_expected_at("comma", pos); - } - - if let Some(key) = &entry.key { - comma_and_keyless = false; - p.deco(Deco::Name.with_span(key.span)); - } - - dict.0.push(entry); - if p.eof() { - break; - } - - if p.eat_if(Token::Comma) { - comma_and_keyless = false; - } else { - missing_coma = Some(behind); - } - } - } - - let coercible = comma_and_keyless && !dict.0.is_empty(); - (dict, coercible) -} - -/// Parse a single entry in a dictionary. -fn dict_entry(p: &mut Parser) -> Option { - if let Some(ident) = p.span_if(ident) { - match p.peek() { - // Key-value pair. - Some(Token::Colon) => { - p.eat_assert(Token::Colon); - p.span_if(expr).map(|expr| LitDictEntry { - key: Some(ident.map(|id| DictKey::Str(id.0))), - expr, - }) - } - - // Function call. - Some(Token::LeftParen) => Some(LitDictEntry { - key: None, - expr: { - let start = ident.span.start; - let call = paren_call(p, ident); - Expr::Call(call).with_span(start .. p.last_end()) - }, - }), - - // Just an identifier. - _ => Some(LitDictEntry { - key: None, - expr: ident.map(|id| Expr::Lit(Lit::Ident(id))), - }), - } - } else { - p.span_if(expr).map(|expr| LitDictEntry { key: None, expr }) - } -} - /// Parse an expression: `term (+ term)*`. fn expr(p: &mut Parser) -> Option { binops(p, term, |token| match token { @@ -418,19 +354,6 @@ fn content(p: &mut Parser) -> SynTree { tree } -/// Parse a parenthesized expression: `(a + b)`, `(1, name: "value"). -fn parenthesized(p: &mut Parser) -> Expr { - p.start_group(Group::Paren); - let (dict, coercible) = dict_contents(p); - let expr = if coercible { - dict.0.into_iter().next().expect("dict is coercible").expr.v - } else { - Expr::Lit(Lit::Dict(dict)) - }; - p.end_group(); - expr -} - /// Parse an identifier. fn ident(p: &mut Parser) -> Option { p.eat_map(|token| match token { diff --git a/src/parse/tests.rs b/src/parse/tests.rs index 230a5dbab..0c8998b5e 100644 --- a/src/parse/tests.rs +++ b/src/parse/tests.rs @@ -5,7 +5,6 @@ use std::fmt::Debug; use super::parse; use crate::color::RgbaColor; use crate::diag::{Diag, Level, Pass}; -use crate::eval::DictKey; use crate::geom::Unit; use crate::syntax::*; @@ -154,21 +153,38 @@ fn Unary(op: impl Into>, expr: impl Into>) -> Expr { }) } +macro_rules! Array { + (@$($expr:expr),* $(,)?) => { + vec![$(into!($expr)),*] + }; + ($($tts:tt)*) => (Expr::Lit(Lit::Array(Array![@$($tts)*]))); +} + macro_rules! Dict { - (@$($a:expr $(=> $b:expr)?),* $(,)?) => { - LitDict(vec![$(#[allow(unused)] { - let key: Option> = None; - let expr = $a; - $( - let key = Some(into!($a).map(|s: &str| s.into())); - let expr = $b; - )? - LitDictEntry { key, expr: into!(expr) } - }),*]) + (@$($name:expr => $expr:expr),* $(,)?) => { + vec![$(Named { + name: into!($name).map(|s: &str| Ident(s.into())), + expr: into!($expr) + }),*] }; ($($tts:tt)*) => (Expr::Lit(Lit::Dict(Dict![@$($tts)*]))); } +macro_rules! Args { + (@$a:expr) => { + Argument::Pos(into!($a)) + }; + (@$a:expr => $b:expr) => { + Argument::Named(Named { + name: into!($a).map(|s: &str| Ident(s.into())), + expr: into!($b) + }) + }; + ($($a:expr $(=> $b:expr)?),* $(,)?) => { + vec![$(Args!(@$a $(=> $b)?)),*] + }; +} + macro_rules! Content { (@$($node:expr),* $(,)?) => (vec![$(into!($node)),*]); ($($tts:tt)*) => (Expr::Lit(Lit::Content(Content![@$($tts)*]))); @@ -188,10 +204,6 @@ macro_rules! Call { ($($tts:tt)*) => (SynNode::Expr(Call!(@$($tts)*))); } -macro_rules! Args { - ($($tts:tt)*) => (Dict![@$($tts)*]); -} - #[test] fn test_parse_comments() { // In body. @@ -316,10 +328,9 @@ fn test_parse_groups() { errors: [S(1..2, "expected function name, found closing paren"), S(2..2, "expected closing bracket")]); - t!("[v {]}" - nodes: [Call!("v", Args![Content![]])], - errors: [S(4..4, "expected closing brace"), - S(5..6, "unexpected closing brace")]); + t!("[v {*]_" + nodes: [Call!("v", Args![Content![Strong]]), Emph], + errors: [S(5..5, "expected closing brace")]); // Test brace group. t!("{1 + [}" @@ -329,7 +340,7 @@ fn test_parse_groups() { // Test subheader group. t!("[v (|u )]" - nodes: [Call!("v", Args![Dict![], Content![Call!("u")]])], + nodes: [Call!("v", Args![Array![], Content![Call!("u")]])], errors: [S(4..4, "expected closing paren"), S(7..8, "expected expression, found closing paren")]); } @@ -348,6 +359,12 @@ fn test_parse_blocks() { nodes: [], errors: [S(1..1, "expected expression"), S(3..5, "expected expression, found invalid token")]); + + // Too much stuff. + t!("{1 #{} end" + nodes: [Block(Int(1)), Space, Text("end")], + errors: [S(3..4, "unexpected hex value"), + S(4..5, "unexpected opening brace")]); } #[test] @@ -385,7 +402,7 @@ fn test_parse_bracket_funcs() { nodes: [Call!("", Args![Int(1)])], errors: [S(1..2, "expected function name, found hex value")]); - // String header eats closing bracket. + // String in header eats closing bracket. t!(r#"[v "]"# nodes: [Call!("v", Args![Str("]")])], errors: [S(5..5, "expected quote"), @@ -400,8 +417,8 @@ fn test_parse_bracket_funcs() { #[test] fn test_parse_chaining() { // Basic. - t!("[a | b]" Call!("a", Args![Content![Call!("b")]])); - t!("[a | b | c]" Call!("a", Args![Content![ + t!("[a | b]" Call!("a", Args![Content![Call!("b")]])); + t!("[a|b|c]" Call!("a", Args![Content![ Call!("b", Args![Content![Call!("c")]]) ]])); @@ -428,16 +445,14 @@ fn test_parse_chaining() { #[test] fn test_parse_arguments() { // Bracket functions. - t!("[v 1]" Call!("v", Args![Int(1)])); - t!("[v 1,]" Call!("v", Args![Int(1)])); t!("[v a]" Call!("v", Args![Id("a")])); - t!("[v a,]" Call!("v", Args![Id("a")])); + t!("[v 1,]" Call!("v", Args![Int(1)])); t!("[v a:2]" Call!("v", Args!["a" => Int(2)])); - // Parenthesized function with nested dictionary literal. + // Parenthesized function with nested array literal. t!(r#"{f(1, a: (2, 3), #004, b: "five")}"# Block(Call!(@"f", Args![ Int(1), - "a" => Dict![Int(2), Int(3)], + "a" => Array![Int(2), Int(3)], Color(RgbaColor::new(0, 0, 0x44, 0xff)), "b" => Str("five"), ]))); @@ -447,56 +462,111 @@ fn test_parse_arguments() { nodes: [Call!("v", Args![])], errors: [S(3..5, "expected expression, found end of block comment")]); + // Bad expression. + t!("[v a:1:]" + nodes: [Call!("v", Args!["a" => Int(1)])], + errors: [S(6..7, "expected expression, found colon")]); + // Missing comma between arguments. t!("[v 1 2]" nodes: [Call!("v", Args![Int(1), Int(2)])], errors: [S(4..4, "expected comma")]); - // Missing expression after name. - t!("[v a:]" - nodes: [Call!("v", Args![])], - errors: [S(5..5, "expected expression")]); - - // Bad expression after name. - t!("[v a:1:]" - nodes: [Call!("v", Args!["a" => Int(1)])], - errors: [S(6..7, "expected expression, found colon")]); - - // Name has to be identifier. Number parsed as positional argument. + // Name has to be identifier. t!("[v 1:]" - nodes: [Call!("v", Args![Int(1)])], - errors: [S(4..5, "expected expression, found colon")]); + nodes: [Call!("v", Args![])], + errors: [S(3..4, "name must be identifier"), + S(5..5, "expected expression")]); - // Parsed as two positional arguments. + // Name has to be identifier. t!("[v 1:2]" - nodes: [Call!("v", Args![Int(1), Int(2)])], - errors: [S(4..5, "expected expression, found colon"), - S(4..4, "expected comma")]); + nodes: [Call!("v", Args![])], + errors: [S(3..4, "name must be identifier")]); } #[test] -fn test_parse_dict_literals() { - // Basic. - t!("{()}" Block(Dict![])); +fn test_parse_arrays() { + // Empty array. + t!("{()}" Block(Array![])); - // With spans. - t!("{(1, two: 2)}" - nodes: [S(0..13, Block(Dict![ - S(2..3, Int(1)), - S(5..8, "two") => S(10..11, Int(2)), - ]))], + // Array with one item and trailing comma + spans. + t!("{-(1,)}" + nodes: [S(0..7, Block(Unary( + S(1..2, Neg), + S(2..6, Array![S(3..4, Int(1))]) + )))], spans: true); + // Array with three items and trailing comma. + t!(r#"{("one", 2, #003,)}"# Block(Array![ + Str("one"), + Int(2), + Color(RgbaColor::new(0, 0, 0x33, 0xff)) + ])); + // Unclosed. t!("{(}" - nodes: [Block(Dict![])], + nodes: [Block(Array![])], errors: [S(2..2, "expected closing paren")]); + + // Missing comma + invalid token. + t!("{(1*/2)}" + nodes: [Block(Array![Int(1), Int(2)])], + errors: [S(3..5, "expected expression, found end of block comment"), + S(3..3, "expected comma")]); + + // Invalid token. + t!("{(1, 1u 2)}" + nodes: [Block(Array![Int(1), Int(2)])], + errors: [S(5..7, "expected expression, found invalid token")]); + + // Coerced to expression with leading comma. + t!("{(,1)}" + nodes: [Block(Int(1))], + errors: [S(2..3, "expected expression, found comma")]); + + // Missing expression after name makes this an array. + t!("{(a:)}" + nodes: [Block(Array![])], + errors: [S(4..4, "expected expression")]); + + // Expected expression, found named pair. + t!("{(1, b: 2)}" + nodes: [Block(Array![Int(1)])], + errors: [S(5..9, "expected expression, found named pair")]); +} + +#[test] +fn test_parse_dictionaries() { + // Empty dictionary. + t!("{(:)}" Block(Dict![])); + + // Dictionary with two pairs + spans. + t!("{(one: 1, two: 2)}" + nodes: [S(0..18, Block(Dict![ + S(2..5, "one") => S(7..8, Int(1)), + S(10..13, "two") => S(15..16, Int(2)), + ]))], + spans: true); + + // Expected named pair, found expression. + t!("{(a: 1, b)}" + nodes: [Block(Dict!["a" => Int(1)])], + errors: [S(8..9, "expected named pair, found expression")]); + + // Dictionary marker followed by more stuff. + t!("{(:1 b:2, true::)}" + nodes: [Block(Dict!["b" => Int(2)])], + errors: [S(3..4, "expected named pair, found expression"), + S(4..4, "expected comma"), + S(10..14, "name must be identifier"), + S(15..16, "expected expression, found colon")]); } #[test] fn test_parse_expressions() { - // Parenthesis. - t!("{(x)}" Block(Id("x"))); + // Parentheses. + t!("{(x)}{(1)}" Block(Id("x")), Block(Int(1))); // Unary operations. t!("{-1}" Block(Unary(Neg, Int(1)))); @@ -561,4 +631,12 @@ fn test_parse_values() { t!("{#a5}" nodes: [Block(Color(RgbaColor::new(0, 0, 0, 0xff)))], errors: [S(1..4, "invalid color")]); + + // Content. + t!("{{*Hi*}}" Block(Content![Strong, Text("Hi"), Strong])); + + // Invalid tokens. + t!("{1u}" + nodes: [], + errors: [S(1..3, "expected expression, found invalid token")]); } diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index d79197630..a9692a587 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -477,13 +477,9 @@ mod tests { } #[test] - fn test_length_from_str_parses_correct_value_and_unit() { + fn test_length_from_str() { assert_eq!(parse_length("2.5cm"), Some((2.5, Cm))); assert_eq!(parse_length("1.e+2cm"), Some((100.0, Cm))); - } - - #[test] - fn test_length_from_str_works_with_non_ascii_chars() { assert_eq!(parse_length("123🚚"), None); } diff --git a/src/prelude.rs b/src/prelude.rs index b9cf561d4..5d446e8cf 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,7 +2,10 @@ pub use crate::diag::{Feedback, Pass}; #[doc(no_inline)] -pub use crate::eval::{Args, Dict, Eval, EvalContext, Value, ValueDict}; +pub use crate::eval::{ + Args, CastResult, Eval, EvalContext, Value, ValueAny, ValueArray, ValueContent, + ValueDict, +}; pub use crate::geom::*; #[doc(no_inline)] pub use crate::layout::LayoutNode; diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 4c6ce8723..905ade04e 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -2,7 +2,6 @@ use super::*; use crate::color::RgbaColor; -use crate::eval::DictKey; use crate::geom::Unit; /// An expression. @@ -24,10 +23,22 @@ pub struct ExprCall { /// The name of the function. pub name: Spanned, /// The arguments to the function. - /// - /// In case of a bracketed invocation with a body, the body is _not_ - /// included in the span for the sake of clearer error messages. - pub args: Spanned, + pub args: Spanned, +} + +/// The arguments to a function: `12, draw: false`. +/// +/// In case of a bracketed invocation with a body, the body is _not_ +/// included in the span for the sake of clearer error messages. +pub type Arguments = Vec; + +/// An argument to a function call: `12` or `draw: false`. +#[derive(Debug, Clone, PartialEq)] +pub enum Argument { + /// A positional arguments. + Pos(Spanned), + /// A named argument. + Named(Named), } /// A unary operation: `-x`. @@ -92,28 +103,25 @@ pub enum Lit { Color(RgbaColor), /// A string literal: `"hello!"`. Str(String), - /// A dictionary literal: `(false, 12cm, greeting: "hi")`. - Dict(LitDict), + /// An array literal: `(1, "hi", 12cm)`. + Array(Array), + /// A dictionary literal: `(color: #f79143, pattern: dashed)`. + Dict(Dict), /// A content literal: `{*Hello* there!}`. Content(SynTree), } -/// A dictionary literal: `(false, 12cm, greeting: "hi")`. -#[derive(Debug, Default, Clone, PartialEq)] -pub struct LitDict(pub Vec); +/// An array literal: `(1, "hi", 12cm)`. +pub type Array = SpanVec; -/// An entry in a dictionary literal: `false` or `greeting: "hi"`. +/// A dictionary literal: `(color: #f79143, pattern: dashed)`. +pub type Dict = Vec; + +/// A pair of a name and an expression: `pattern: dashed`. #[derive(Debug, Clone, PartialEq)] -pub struct LitDictEntry { - /// The key of the entry if there was one: `greeting`. - pub key: Option>, - /// The value of the entry: `"hi"`. +pub struct Named { + /// The name: `pattern`. + pub name: Spanned, + /// The right-hand side of the pair: `dashed`. pub expr: Spanned, } - -impl LitDict { - /// Create an empty dict literal. - pub fn new() -> Self { - Self::default() - } -} diff --git a/src/syntax/node.rs b/src/syntax/node.rs index b7691a70e..cee810a2a 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -30,7 +30,7 @@ pub enum SynNode { Expr(Expr), } -/// A section heading: `# ...`. +/// A section heading: `# Introduction`. #[derive(Debug, Clone, PartialEq)] pub struct NodeHeading { /// The section depth (numer of hashtags minus 1). diff --git a/tests/ref/func-rgb.png b/tests/ref/func-rgb.png index 0f34d661d..239a9e5a5 100644 Binary files a/tests/ref/func-rgb.png and b/tests/ref/func-rgb.png differ diff --git a/tests/typ/func-font-error.typ b/tests/typ/func-font-error.typ index 492ef9df1..b75a4fb7f 100644 --- a/tests/typ/func-font-error.typ +++ b/tests/typ/func-font-error.typ @@ -6,12 +6,16 @@ // Wrong types. [font style: bold, weight: "thin", serif: 0] +// Weight out of range. +[font weight: 2700] + // Non-existing argument. [font something: "invalid"] // compare-ref: false // error: 4:7-4:12 unexpected argument -// error: 7:14-7:18 invalid font style +// error: 7:14-7:18 expected font style, found font weight // error: 7:28-7:34 expected font weight, found string -// error: 7:43-7:44 expected family or list of families, found integer -// error: 10:7-10:27 unexpected argument +// error: 7:43-7:44 expected font family or array of font families, found integer +// warning: 10:15-10:19 must be between 100 and 900 +// error: 13:7-13:27 unexpected argument diff --git a/tests/typ/func-font-fallback.typ b/tests/typ/func-font-fallback.typ index 9b60d46c0..c6dd81f05 100644 --- a/tests/typ/func-font-fallback.typ +++ b/tests/typ/func-font-fallback.typ @@ -4,15 +4,17 @@ Emoji: 🏀 // CMU Serif + Noto Emoji. -[font "CMU Serif", "Noto Emoji"][Emoji: 🏀] +[font "CMU Serif", "Noto Emoji"][ + Emoji: 🏀 +] // Class definitions. -[font math: ("CMU Serif", "Latin Modern Math", "Noto Emoji")] -[font math][Math: ∫ α + β ➗ 3] +[font serif: ("CMU Serif", "Latin Modern Math", "Noto Emoji")] +[font serif][ + Math: ∫ α + β ➗ 3 +] -// Class redefinition. +// Class definition reused. [font sans-serif: "Noto Emoji"] [font sans-serif: ("Archivo", sans-serif)] New sans-serif. 🚀 - -// TODO: Add tests for other scripts. diff --git a/tests/typ/func-page-error.typ b/tests/typ/func-page-error.typ index 1b2db60df..21370fa8b 100644 --- a/tests/typ/func-page-error.typ +++ b/tests/typ/func-page-error.typ @@ -7,5 +7,5 @@ [page main-dir: ltr] // compare-ref: false -// error: 4:7-4:18 invalid paper +// error: 4:7-4:18 unknown variable // error: 7:17-7:20 aligned axis diff --git a/tests/typ/func-page-metrics.typ b/tests/typ/func-page-metrics.typ index 7e0bc2f84..3b54d13f8 100644 --- a/tests/typ/func-page-metrics.typ +++ b/tests/typ/func-page-metrics.typ @@ -22,4 +22,4 @@ [page margins: 0pt, left: 40pt][Overriden] // Flip the page. -[page a10, flip: true][Flipped] +[page "a10", flip: true][Flipped] diff --git a/tests/typ/func-rgb.typ b/tests/typ/func-rgb.typ index b47039a28..96c23ebdf 100644 --- a/tests/typ/func-rgb.typ +++ b/tests/typ/func-rgb.typ @@ -1,7 +1,7 @@ // Test the `rgb` function. // Check the output. -[rgb 0.0, 0.3, 0.7] [val #004db3] +[rgb 0.0, 0.3, 0.7] // Alpha channel. [rgb 1.0, 0.0, 0.0, 0.5] @@ -15,9 +15,8 @@ // Missing all components. [rgb] -// error: 4:22-4:25 unknown function -// error: 10:6-10:9 should be between 0.0 and 1.0 -// error: 10:11-10:15 should be between 0.0 and 1.0 +// warning: 10:6-10:9 must be between 0.0 and 1.0 +// warning: 10:11-10:15 must be between 0.0 and 1.0 // error: 13:6-13:10 missing argument: blue component // error: 16:5-16:5 missing argument: red component // error: 16:5-16:5 missing argument: green component