summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/color.rs4
-rw-r--r--src/eval/args.rs246
-rw-r--r--src/eval/dict.rs522
-rw-r--r--src/eval/mod.rs95
-rw-r--r--src/eval/scope.rs18
-rw-r--r--src/eval/value.rs481
-rw-r--r--src/geom/length.rs12
-rw-r--r--src/library/insert.rs14
-rw-r--r--src/library/layout.rs201
-rw-r--r--src/library/mod.rs81
-rw-r--r--src/library/style.rs201
-rw-r--r--src/parse/collection.rs142
-rw-r--r--src/parse/mod.rs105
-rw-r--r--src/parse/tests.rs194
-rw-r--r--src/parse/tokens.rs6
-rw-r--r--src/prelude.rs5
-rw-r--r--src/syntax/expr.rs52
-rw-r--r--src/syntax/node.rs2
-rw-r--r--tests/ref/func-rgb.pngbin3153 -> 2727 bytes
-rw-r--r--tests/typ/func-font-error.typ10
-rw-r--r--tests/typ/func-font-fallback.typ14
-rw-r--r--tests/typ/func-page-error.typ2
-rw-r--r--tests/typ/func-page-metrics.typ2
-rw-r--r--tests/typ/func-rgb.typ7
24 files changed, 1097 insertions, 1319 deletions
diff --git a/src/color.rs b/src/color.rs
index 9dc31c30..f349fdb4 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 43c30daf..ea248ec4 100644
--- a/src/eval/args.rs
+++ b/src/eval/args.rs
@@ -1,184 +1,138 @@
-//! Simplifies argument parsing.
+use super::*;
+
+/// Evaluated arguments to a function.
+#[derive(Debug)]
+pub struct Args {
+ span: Span,
+ pos: SpanVec<Value>,
+ named: Vec<(Spanned<String>, Spanned<Value>)>,
+}
-use std::mem;
+impl Eval for Spanned<&Arguments> {
+ type Output = Args;
-use super::{Conv, EvalContext, RefKey, TryFromValue, Value, ValueDict};
-use crate::diag::Diag;
-use crate::syntax::{Span, SpanVec, Spanned, WithSpan};
+ fn eval(self, ctx: &mut EvalContext) -> Self::Output {
+ let mut pos = vec![];
+ let mut named = vec![];
-/// A wrapper around a dictionary value that simplifies argument parsing in
-/// functions.
-pub struct Args(pub Spanned<ValueDict>);
+ 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),
+ ));
+ }
+ }
+ }
-impl Args {
- /// Retrieve and remove the argument associated with the given key if there
- /// is any.
- ///
- /// 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<T>
- where
- K: Into<RefKey<'a>>,
- T: TryFromValue,
- {
- 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,
- )
- })
+ Args { span: self.span, pos, named }
}
+}
- /// 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<T>
+impl Args {
+ /// Find the first convertible positional argument.
+ pub fn find<T>(&mut self, ctx: &mut EvalContext) -> Option<T>
where
- K: Into<RefKey<'a>>,
- T: TryFromValue,
+ T: Cast<Spanned<Value>>,
{
- 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
- }
+ self.pos.iter_mut().find_map(move |slot| try_cast(ctx, slot))
}
- /// Retrieve and remove the first matching positional argument.
- pub fn find<T>(&mut self) -> Option<T>
+ /// Find the first convertible positional argument, producing an error if
+ /// no match was found.
+ pub fn require<T>(&mut self, ctx: &mut EvalContext, what: &str) -> Option<T>
where
- T: TryFromValue,
+ T: Cast<Spanned<Value>>,
{
- 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);
- }
+ let found = self.find(ctx);
+ if found.is_none() {
+ ctx.diag(error!(self.span, "missing argument: {}", what));
}
- None
+ found
}
- /// Retrieve and remove all matching positional arguments.
- pub fn find_all<T>(&mut self) -> impl Iterator<Item = T> + '_
+ /// Filter out all convertible positional arguments.
+ pub fn filter<'a, T>(
+ &'a mut self,
+ ctx: &'a mut EvalContext,
+ ) -> impl Iterator<Item = T> + 'a
where
- T: TryFromValue,
+ T: Cast<Spanned<Value>>,
{
- 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
- })
+ self.pos.iter_mut().filter_map(move |slot| try_cast(ctx, slot))
}
- /// Retrieve and remove all matching named arguments.
- pub fn find_all_str<T>(&mut self) -> impl Iterator<Item = (String, T)> + '_
+ /// Convert the value for the given named argument.
+ ///
+ /// Generates an error if the conversion fails.
+ pub fn get<'a, T>(&mut self, ctx: &mut EvalContext, name: &str) -> Option<T>
where
- T: TryFromValue,
+ T: Cast<Spanned<Value>>,
{
- 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
- })
+ let index = self.named.iter().position(|(k, _)| k.v.as_str() == name)?;
+ let value = self.named.remove(index).1;
+ cast(ctx, value)
}
- /// 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"));
+ /// 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"));
+ }
}
}
}
-fn conv_diag<T>(conv: Conv<T>, diags: &mut SpanVec<Diag>, span: Span) -> Option<T> {
- 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<T>(ctx: &mut EvalContext, value: Spanned<Value>) -> Option<T>
+where
+ T: Cast<Spanned<Value>>,
+{
+ 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<T>(conv: Conv<T>, slot: &mut Spanned<Value>, span: Span) -> Option<T> {
- 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<T>(ctx: &mut EvalContext, slot: &mut Spanned<Value>) -> Option<T>
+where
+ T: Cast<Spanned<Value>>,
+{
+ // 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<Value> {
- 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::<String>(), Some("hi".to_string()));
- assert_eq!(args.0.v.len(), 1);
- assert_eq!(args.find::<bool>(), 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::<bool>().collect::<Vec<_>>(), [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 efa5cb37..00000000
--- 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<V> {
- nums: BTreeMap<u64, V>,
- strs: BTreeMap<String, V>,
- lowest_free: u64,
-}
-
-impl<V> Dict<V> {
- /// 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<RefKey<'a>>,
- {
- 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<RefKey<'a>>,
- {
- 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<K>(&mut self, key: K, value: V)
- where
- K: Into<DictKey>,
- {
- 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<V>
- where
- K: Into<RefKey<'a>>,
- {
- 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<K> for Dict<V>
-where
- K: Into<RefKey<'a>>,
-{
- type Output = V;
-
- fn index(&self, index: K) -> &Self::Output {
- self.get(index).expect("key not in dict")
- }
-}
-
-impl<V: Eq> Eq for Dict<V> {}
-
-impl<V: PartialEq> PartialEq for Dict<V> {
- fn eq(&self, other: &Self) -> bool {
- self.iter().eq(other.iter())
- }
-}
-
-impl<V> Default for Dict<V> {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl<V: Debug> Debug for Dict<V> {
- 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<V> Dict<V> {
- /// Iterator over all borrowed keys and values.
- pub fn iter(&self) -> impl Iterator<Item = (RefKey, &V)> {
- self.into_iter()
- }
-
- /// Iterator over all borrowed keys and values.
- pub fn iter_mut(&mut self) -> impl Iterator<Item = (RefKey, &mut V)> {
- self.into_iter()
- }
-
- /// Iterate over all values in the dictionary.
- pub fn values(&self) -> impl Iterator<Item = &V> {
- 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<Item = &mut V> {
- 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<Item = V> {
- 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<u64, V> {
- self.nums.iter()
- }
-
- /// Iterate mutably over the number key-value pairs.
- pub fn nums_mut(&mut self) -> std::collections::btree_map::IterMut<u64, V> {
- self.nums.iter_mut()
- }
-
- /// Iterate over the number key-value pairs.
- pub fn into_nums(self) -> std::collections::btree_map::IntoIter<u64, V> {
- self.nums.into_iter()
- }
-
- /// Iterate over the string key-value pairs.
- pub fn strs(&self) -> std::collections::btree_map::Iter<String, V> {
- self.strs.iter()
- }
-
- /// Iterate mutably over the string key-value pairs.
- pub fn strs_mut(&mut self) -> std::collections::btree_map::IterMut<String, V> {
- self.strs.iter_mut()
- }
-
- /// Iterate over the string key-value pairs.
- pub fn into_strs(self) -> std::collections::btree_map::IntoIter<String, V> {
- self.strs.into_iter()
- }
-}
-
-impl<V> Extend<(DictKey, V)> for Dict<V> {
- fn extend<T: IntoIterator<Item = (DictKey, V)>>(&mut self, iter: T) {
- for (key, value) in iter.into_iter() {
- self.insert(key, value);
- }
- }
-}
-
-impl<V> FromIterator<(DictKey, V)> for Dict<V> {
- fn from_iter<T: IntoIterator<Item = (DictKey, V)>>(iter: T) -> Self {
- let mut v = Self::new();
- v.extend(iter);
- v
- }
-}
-
-impl<V> IntoIterator for Dict<V> {
- type Item = (DictKey, V);
- type IntoIter = std::iter::Chain<
- std::iter::Map<
- std::collections::btree_map::IntoIter<u64, V>,
- fn((u64, V)) -> (DictKey, V),
- >,
- std::iter::Map<
- std::collections::btree_map::IntoIter<String, V>,
- 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<V> {
- 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<V> {
- 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<RefKey<'_>> 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<u64> for DictKey {
- fn from(num: u64) -> Self {
- Self::Num(num)
- }
-}
-
-impl From<String> 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<u64> 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<V> {
- pub key_span: Span,
- pub value: Spanned<V>,
-}
-
-impl<V> SpannedEntry<V> {
- /// Create a new entry.
- pub fn new(key: Span, val: Spanned<V>) -> Self {
- Self { key_span: key, value: val }
- }
-
- /// Create an entry with the same span for key and value.
- pub fn value(val: Spanned<V>) -> Self {
- Self { key_span: val.span, value: val }
- }
-
- /// Convert from `&SpannedEntry<T>` 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<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> {
- SpannedEntry {
- key_span: self.key_span,
- value: self.value.map(f),
- }
- }
-}
-
-impl<V: Debug> Debug for SpannedEntry<V> {
- 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::<Vec<_>>(), [
- "(",
- r#" 10: "hello","#,
- r#" "sp ace": "quotes","#,
- r#" twenty: "there","#,
- ")",
- ]);
- }
-}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 56946210..62bd444c 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> {
- type Output = ValueDict;
+
+impl Eval for Spanned<&Array> {
+ type Output = ValueArray;
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
- let mut dict = ValueDict::new();
+ self.v.iter().map(|expr| expr.as_ref().eval(ctx)).collect()
+ }
+}
- 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));
- }
- }
+impl Eval for Spanned<&Dict> {
+ type Output = ValueDict;
- dict
+ fn eval(self, ctx: &mut EvalContext) -> Self::Output {
+ 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 c0624393..c9ce1423 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<String, ValueFunc>,
+ values: HashMap<String, Value>,
}
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<String>, function: ValueFunc) {
- self.functions.insert(name.into(), function);
+ /// Store the value for the given variable.
+ pub fn set(&mut self, var: impl Into<String>, value: impl Into<Value>) {
+ 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 a009e891..d1dcdcfa 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<T>(self) -> CastResult<T, Self>
+ where
+ T: Cast<Value>,
+ {
+ 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("<error>"),
}
}
}
-/// A dictionary of values.
-///
-/// # Example
-/// ```typst
-/// (false, 12cm, greeting: "hi")
-/// ```
-pub type ValueDict = Dict<SpannedEntry<Value>>;
+/// An array value: `(1, "hi", 12cm)`.
+pub type ValueArray = Vec<Value>;
-/// 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
-#[derive(Clone)]
-pub struct ValueFunc(pub Rc<Func>);
+/// A dictionary value: `(color: #f79143, pattern: dashed)`.
+pub type ValueDict = HashMap<String, Value>;
+
+/// A content value: `{*Hi* there}`.
+pub type ValueContent = SynTree;
-/// The signature of executable functions.
-type Func = dyn Fn(Args, &mut EvalContext) -> Value;
+/// A wrapper around a reference-counted executable function.
+#[derive(Clone)]
+pub struct ValueFunc(Rc<dyn Fn(&mut EvalContext, &mut Args) -> Value>);
impl ValueFunc {
/// Create a new function value from a rust function or closure.
- pub fn new<F>(f: F) -> Self
+ pub fn new<F>(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<Value>) -> Conv<Self>;
+/// A wrapper around a dynamic value.
+pub struct ValueAny(Box<dyn Bounds>);
+
+impl ValueAny {
+ /// Create a new instance from any value that satisifies the required bounds.
+ pub fn new<T>(any: T) -> Self
+ where
+ T: Type + Debug + Clone + PartialEq + 'static,
+ {
+ Self(Box::new(any))
+ }
+
+ /// Whether the wrapped type is `T`.
+ pub fn is<T: 'static>(&self) -> bool {
+ self.0.as_any().is::<T>()
+ }
+
+ /// Try to downcast to a specific type.
+ pub fn downcast<T: 'static>(self) -> Result<T, Self> {
+ if self.is::<T>() {
+ Ok(*self.0.into_any().downcast().unwrap())
+ } else {
+ Err(self)
+ }
+ }
+
+ /// Try to downcast to a reference to a specific type.
+ pub fn downcast_ref<T: 'static>(&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<T> {
- /// Success conversion.
- Ok(T),
- /// Sucessful conversion with a warning.
- Warn(T, Diag),
- /// Unsucessful conversion, gives back the value alongside the error.
- Err(Value, Diag),
+impl Clone for ValueAny {
+ fn clone(&self) -> Self {
+ Self(self.0.dyn_clone())
+ }
}
-impl<T> Conv<T> {
- /// Map the conversion result.
- pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Conv<U> {
- 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),
- }
+impl PartialEq for ValueAny {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.dyn_eq(other)
}
}
-impl<T: TryFromValue> TryFromValue for Spanned<T> {
- fn try_from_value(value: Spanned<Value>) -> Conv<Self> {
- let span = value.span;
- T::try_from_value(value).map(|v| v.with_span(span))
+impl Debug for ValueAny {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.fmt(f)
}
}
-/// A value type that matches [identifier](Value::Ident) and [string](Value::Str) values.
-pub struct StringLike(pub String);
+trait Bounds: Debug + 'static {
+ fn as_any(&self) -> &dyn Any;
+ fn into_any(self: Box<Self>) -> Box<dyn Any>;
+ fn dyn_eq(&self, other: &ValueAny) -> bool;
+ fn dyn_clone(&self) -> Box<dyn Bounds>;
+ fn dyn_type_name(&self) -> &'static str;
+}
-impl From<StringLike> for String {
- fn from(like: StringLike) -> String {
- like.0
+impl<T> Bounds for T
+where
+ T: Type + Debug + Clone + PartialEq + 'static,
+{
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn into_any(self: Box<Self>) -> Box<dyn Any> {
+ self
+ }
+
+ fn dyn_eq(&self, other: &ValueAny) -> bool {
+ if let Some(other) = other.downcast_ref::<Self>() {
+ self == other
+ } else {
+ false
+ }
+ }
+
+ fn dyn_clone(&self) -> Box<dyn Bounds> {
+ Box::new(self.clone())
+ }
+
+ fn dyn_type_name(&self) -> &'static str {
+ T::TYPE_NAME
}
}
-impl Deref for StringLike {
- type Target = str;
+/// Types that can be stored in values.
+pub trait Type {
+ /// The name of the type.
+ const TYPE_NAME: &'static str;
+}
- fn deref(&self) -> &str {
- self.0.as_str()
+impl<T> Type for Spanned<T>
+where
+ T: Type,
+{
+ const TYPE_NAME: &'static str = T::TYPE_NAME;
+}
+
+/// Cast from a value to a specific type.
+pub trait Cast<V>: Type + Sized {
+ /// Try to cast the value into an instance of `Self`.
+ fn cast(value: V) -> CastResult<Self, V>;
+}
+
+/// The result of casting a value to a specific type.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum CastResult<T, V> {
+ /// The value was cast successfully.
+ Ok(T),
+ /// 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<T, V> CastResult<T, V> {
+ /// Access the conversion resulting, discarding a possibly existing warning.
+ pub fn ok(self) -> Option<T> {
+ match self {
+ CastResult::Ok(t) | CastResult::Warn(t, _) => Some(t),
+ CastResult::Err(_) => None,
+ }
}
}
-/// 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<Value>) -> $crate::eval::Conv<Self> {
- 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<T> Cast<Spanned<Value>> for T
+where
+ T: Cast<Value>,
+{
+ fn cast(value: Spanned<Value>) -> CastResult<Self, Spanned<Value>> {
+ let span = value.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)),
}
- };
+ }
+}
+
+impl<T> Cast<Spanned<Value>> for Spanned<T>
+where
+ T: Cast<Value>,
+{
+ fn cast(value: Spanned<Value>) -> CastResult<Self, Spanned<Value>> {
+ 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)),
+ }
+ }
}
-/// 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<Value>) -> $crate::eval::Conv<Self> {
- 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)
+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;
+ }
+
+ impl From<$type> for Value {
+ fn from(v: $type) -> Self {
+ $variant(v)
+ }
+ }
+
+ impl Cast<Value> for $type {
+ fn cast(value: Value) -> CastResult<Self, Value> {
+ match value {
+ $variant(v) => CastResult::Ok(v),
+ $($pattern => CastResult::Ok($out),)*
+ v => CastResult::Err(v),
}
}
}
};
}
-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! { 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 }
+
+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<Value>) -> Conv<Self> {
- 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))
- }
+}
+
+impl From<&str> for Value {
+ fn from(v: &str) -> Self {
+ Self::Str(v.to_string())
+ }
+}
+
+impl<F> From<F> for Value
+where
+ F: Fn(&mut EvalContext, &mut Args) -> Value + 'static,
+{
+ fn from(func: F) -> Self {
+ Self::Func(ValueFunc::new(func))
+ }
+}
+
+impl From<ValueAny> 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<T>`](From) for [`Value`],
+/// - [`Cast<Value>`](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))
}
- Value::Ident(id) => {
- if let Some(weight) = Self::from_str(&id) {
- Conv::Ok(weight)
- } else {
- Conv::Err(Value::Ident(id), error!("invalid font weight"))
+ }
+
+ impl $crate::eval::Cast<$crate::eval::Value> for $type {
+ fn cast(
+ value: $crate::eval::Value,
+ ) -> $crate::eval::CastResult<Self, $crate::eval::Value> {
+ use $crate::eval::*;
+
+ #[allow(unreachable_code)]
+ match value {
+ $($pattern => CastResult::Ok($out),)*
+ Value::Any(mut any) => {
+ any = match any.downcast::<Self>() {
+ 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),
}
}
- v => {
- let e = error!("expected font weight, found {}", v.ty());
- Conv::Err(v, e)
- }
}
- }
+ };
}
diff --git a/src/geom/length.rs b/src/geom/length.rs
index 061510a1..0e445153 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() {
- 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());
+ fn test_length_unit_conversion() {
+ assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4);
}
#[test]
- fn test_length_unit_conversion() {
- assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4);
+ 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());
}
}
diff --git a/src/library/insert.rs b/src/library/insert.rs
index 6f267b7a..587f96dc 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<String>>(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::<Spanned<String>>(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 c888ea18..ac152d07 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::<SynTree>();
- let first = args.get::<_, Spanned<SpecAlign>>(ctx, 0);
- let second = args.get::<_, Spanned<SpecAlign>>(ctx, 1);
- let hor = args.get::<_, Spanned<SpecAlign>>(ctx, "horizontal");
- let ver = args.get::<_, Spanned<SpecAlign>>(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<Alignment>| (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::<ValueContent>(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<SpecAxis> {
+ fn axis(self) -> Option<SpecAxis> {
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::<SynTree>().unwrap_or_default();
- let width = args.get::<_, Linear>(ctx, "width");
- let height = args.get::<_, Linear>(ctx, "height");
- let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
- let cross = args.get::<_, Spanned<Dir>>(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::<ValueContent>(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<Linear> = 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::<SynTree>();
- 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::<Spanned<String>>(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<Dir>>(ctx, "main-dir");
- let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
- ctx.set_flow(Gen::new(main, cross));
+ let main = args.get(ctx, "main-dir");
+ let cross = args.get(ctx, "cross-dir");
- args.done(ctx);
+ ctx.set_flow(Gen::new(main, cross));
let mut softness = ctx.end_page_group(|_| false);
-
- if let Some(body) = body {
+ if let Some(body) = args.find::<ValueContent>(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 806b0275..1cc7b9e9 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};
-
-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 fontdock::{FontStretch, FontStyle, FontWeight};
+
+use crate::eval::Scope;
+use crate::geom::Dir;
+
+/// 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! {
- align,
- boxed => "box",
- font,
- h,
- image,
- page,
- pagebreak,
- rgb,
- v,
+ std
}
diff --git a/src/library/style.rs b/src/library/style.rs
index 76838046..3bdcdfd4 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::<SynTree>();
- if let Some(linear) = args.find::<Linear>() {
+ if let Some(linear) = args.find::<Linear>(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::<StringLike>().map(|s| s.to_lowercase()).collect();
-
+ let list: Vec<_> = args.filter::<FontFamily>(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<String>);
-
- 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::<StringLike>()
- .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::<ValueContent>(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<FontFamily>);
+
+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<f64>>(ctx, 0, "red component");
- let g = args.need::<_, Spanned<f64>>(ctx, 1, "green component");
- let b = args.need::<_, Spanned<f64>>(ctx, 2, "blue component");
- let a = args.get::<_, Spanned<f64>>(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<Spanned<f64>>, 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 00000000..db267dbe
--- /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<T: 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<Argument> {
+ 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<Argument>);
+ fn push_comma(&mut self) {}
+}
+
+impl Collection for Arguments {
+ fn push_arg(&mut self, _: &mut Parser, arg: Spanned<Argument>) {
+ self.push(arg.v);
+ }
+}
+
+/// State of collection parsing.
+#[derive(Debug)]
+enum State {
+ Unknown,
+ Expr(Spanned<Expr>),
+ 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<Argument>) {
+ 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<Expr>) -> Spanned<Expr> {
+ // 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<Argument>) {
+ 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 7880dd7a..912a34d0 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<SynTree> {
let mut p = Parser::new(src);
@@ -153,6 +155,9 @@ fn block_expr(p: &mut Parser) -> Option<Expr> {
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<Expr> {
/// Parse a parenthesized function call.
fn paren_call(p: &mut Parser, name: Spanned<Ident>) -> 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<LitDictEntry> {
- 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<Expr> {
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<Ident> {
p.eat_map(|token| match token {
diff --git a/src/parse/tests.rs b/src/parse/tests.rs
index 230a5dba..0c8998b5 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<Spanned<UnOp>>, expr: impl Into<Spanned<Expr>>) -> 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<Spanned<DictKey>> = 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![]));
-
- // 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)),
- ]))],
+fn test_parse_arrays() {
+ // Empty array.
+ t!("{()}" Block(Array![]));
+
+ // 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 d7919763..a9692a58 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 b9cf561d..5d446e8c 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 4c6ce872..905ade04 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<Ident>,
/// 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<LitDict>,
+ pub args: Spanned<Arguments>,
+}
+
+/// 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<Argument>;
+
+/// An argument to a function call: `12` or `draw: false`.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Argument {
+ /// A positional arguments.
+ Pos(Spanned<Expr>),
+ /// 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<LitDictEntry>);
+/// An array literal: `(1, "hi", 12cm)`.
+pub type Array = SpanVec<Expr>;
-/// An entry in a dictionary literal: `false` or `greeting: "hi"`.
+/// A dictionary literal: `(color: #f79143, pattern: dashed)`.
+pub type Dict = Vec<Named>;
+
+/// 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<Spanned<DictKey>>,
- /// The value of the entry: `"hi"`.
+pub struct Named {
+ /// The name: `pattern`.
+ pub name: Spanned<Ident>,
+ /// The right-hand side of the pair: `dashed`.
pub expr: Spanned<Expr>,
}
-
-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 b7691a70..cee810a2 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 0f34d661..239a9e5a 100644
--- a/tests/ref/func-rgb.png
+++ b/tests/ref/func-rgb.png
Binary files differ
diff --git a/tests/typ/func-font-error.typ b/tests/typ/func-font-error.typ
index 492ef9df..b75a4fb7 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 9b60d46c..c6dd81f0 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 1b2db60d..21370fa8 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 7e0bc2f8..3b54d13f 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 b47039a2..96c23ebd 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