summaryrefslogtreecommitdiff
path: root/src/compute
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-08-16 22:14:27 +0200
committerLaurenz <laurmaedje@gmail.com>2020-08-16 22:39:21 +0200
commit30f16bbf6431ca0c174ca0a1abaa6a13ef50ab06 (patch)
treef5a5c0adad15840ebe24b39e77ff467862067c91 /src/compute
parent9f6137d8a829fe8f34554623495fa620252a0184 (diff)
Add Value type and replace dyn-nodes with call-exprs 🏗
- In addition to syntax trees there are now `Value`s, which syntax trees can be evaluated into (e.g. the tree is `5+5` and the value is `10`) - Parsing is completely pure, function calls are not parsed into nodes, but into simple call expressions, which are resolved later - Functions aren't dynamic nodes anymore, but simply functions which receive their arguments as a table and the layouting context - Functions may return any `Value` - Layouting is powered by functions which return the new `Commands` value, which informs the layouting engine what to do - When a function returns a non-`Commands` value, the layouter simply dumps the value into the document in monospace
Diffstat (limited to 'src/compute')
-rw-r--r--src/compute/mod.rs5
-rw-r--r--src/compute/scope.rs50
-rw-r--r--src/compute/table.rs379
-rw-r--r--src/compute/value.rs474
4 files changed, 908 insertions, 0 deletions
diff --git a/src/compute/mod.rs b/src/compute/mod.rs
new file mode 100644
index 00000000..ac278243
--- /dev/null
+++ b/src/compute/mod.rs
@@ -0,0 +1,5 @@
+//! Building blocks for the computational part.
+
+pub mod scope;
+pub mod table;
+pub mod value;
diff --git a/src/compute/scope.rs b/src/compute/scope.rs
new file mode 100644
index 00000000..1fd4db0b
--- /dev/null
+++ b/src/compute/scope.rs
@@ -0,0 +1,50 @@
+//! Mapping from identifiers to functions.
+
+use std::collections::HashMap;
+use std::fmt::{self, Debug, Formatter};
+
+use super::value::FuncValue;
+
+/// A map from identifiers to functions.
+pub struct Scope {
+ functions: HashMap<String, FuncValue>,
+ fallback: FuncValue,
+}
+
+impl Scope {
+ // Create a new empty scope with a fallback function that is invoked when no
+ // match is found.
+ pub fn new(fallback: FuncValue) -> Self {
+ Self {
+ functions: HashMap::new(),
+ fallback,
+ }
+ }
+
+ /// Associate the given name with the function.
+ pub fn insert(&mut self, name: impl Into<String>, function: FuncValue) {
+ self.functions.insert(name.into(), function);
+ }
+
+ /// Return the function with the given name if there is one.
+ pub fn func(&self, name: &str) -> Option<&FuncValue> {
+ self.functions.get(name)
+ }
+
+ /// Return the function with the given name or the fallback if there is no
+ /// such function.
+ pub fn func_or_fallback(&self, name: &str) -> &FuncValue {
+ self.func(name).unwrap_or_else(|| self.fallback())
+ }
+
+ /// Return the fallback function.
+ pub fn fallback(&self) -> &FuncValue {
+ &self.fallback
+ }
+}
+
+impl Debug for Scope {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.debug_set().entries(self.functions.keys()).finish()
+ }
+}
diff --git a/src/compute/table.rs b/src/compute/table.rs
new file mode 100644
index 00000000..f11eacfc
--- /dev/null
+++ b/src/compute/table.rs
@@ -0,0 +1,379 @@
+//! A key-value map that can also model array-like structures.
+
+use std::collections::BTreeMap;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::Index;
+
+use crate::syntax::span::{Span, Spanned};
+
+/// A table data structure, which maps from integers (`u64`) or strings to a
+/// generic value type.
+///
+/// The table can be used to model arrays by assigns values to successive
+/// indices from `0..n`. The table type offers special support for this pattern
+/// through the `push` method.
+#[derive(Clone)]
+pub struct Table<V> {
+ nums: BTreeMap<u64, V>,
+ strs: BTreeMap<String, V>,
+ lowest_free: u64,
+}
+
+impl<V> Table<V> {
+ /// Create a new empty table.
+ pub fn new() -> Self {
+ Self {
+ nums: BTreeMap::new(),
+ strs: BTreeMap::new(),
+ lowest_free: 0,
+ }
+ }
+
+ /// The total number of entries in the table.
+ pub fn len(&self) -> usize {
+ self.nums.len() + self.strs.len()
+ }
+
+ /// Whether the table 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<BorrowedKey<'a>>,
+ {
+ match key.into() {
+ BorrowedKey::Num(num) => self.nums.get(&num),
+ BorrowedKey::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<BorrowedKey<'a>>,
+ {
+ match key.into() {
+ BorrowedKey::Num(num) => self.nums.get_mut(&num),
+ BorrowedKey::Str(string) => self.strs.get_mut(string),
+ }
+ }
+
+ /// Insert a value into the table.
+ pub fn insert<K>(&mut self, key: K, value: V)
+ where
+ K: Into<OwnedKey>,
+ {
+ match key.into() {
+ OwnedKey::Num(num) => {
+ self.nums.insert(num, value);
+ if self.lowest_free == num {
+ self.lowest_free += 1;
+ }
+ }
+ OwnedKey::Str(string) => {
+ self.strs.insert(string, value);
+ }
+ }
+ }
+
+ /// Remove the value with the given key from the table.
+ pub fn remove<'a, K>(&mut self, key: K) -> Option<V>
+ where
+ K: Into<BorrowedKey<'a>>,
+ {
+ match key.into() {
+ BorrowedKey::Num(num) => {
+ self.lowest_free = self.lowest_free.min(num);
+ self.nums.remove(&num)
+ }
+ BorrowedKey::Str(string) => self.strs.remove(string),
+ }
+ }
+
+ /// Append a value to the table.
+ ///
+ /// 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;
+ }
+
+ /// Iterate over the number key-value pairs.
+ pub fn nums(&self) -> std::collections::btree_map::Iter<u64, V> {
+ self.nums.iter()
+ }
+
+ /// Iterate over the string key-value pairs.
+ pub fn strs(&self) -> std::collections::btree_map::Iter<String, V> {
+ self.strs.iter()
+ }
+
+ /// Iterate over all values in the table.
+ pub fn values(&self) -> impl Iterator<Item = &V> {
+ self.nums().map(|(_, v)| v).chain(self.strs().map(|(_, v)| v))
+ }
+
+ /// 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 into_strs(self) -> std::collections::btree_map::IntoIter<String, V> {
+ self.strs.into_iter()
+ }
+
+ /// Move into an owned iterator over all values in the table.
+ pub fn into_values(self) -> impl Iterator<Item = V> {
+ self.nums.into_iter().map(|(_, v)| v)
+ .chain(self.strs.into_iter().map(|(_, v)| v))
+ }
+}
+
+impl<'a, K, V> Index<K> for Table<V>
+where
+ K: Into<BorrowedKey<'a>>,
+{
+ type Output = V;
+
+ fn index(&self, index: K) -> &Self::Output {
+ self.get(index).expect("key not in table")
+ }
+}
+
+impl<V> Default for Table<V> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<V: Eq> Eq for Table<V> {}
+
+impl<V: PartialEq> PartialEq for Table<V> {
+ fn eq(&self, other: &Self) -> bool {
+ self.nums().eq(other.nums()) && self.strs().eq(other.strs())
+ }
+}
+
+impl<V: Debug> Debug for Table<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>(&'a dyn Debug, &'a dyn Debug);
+ impl<'a> Debug for Entry<'a> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.0.fmt(f)?;
+ if f.alternate() {
+ f.write_str(" = ")?;
+ } else {
+ f.write_str("=")?;
+ }
+ self.1.fmt(f)
+ }
+ }
+
+ for (key, value) in self.nums() {
+ builder.field(&Entry(&key, &value));
+ }
+
+ for (key, value) in self.strs() {
+ builder.field(&Entry(&key, &value));
+ }
+
+ builder.finish()
+ }
+}
+
+/// The owned variant of a table key.
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum OwnedKey {
+ Num(u64),
+ Str(String),
+}
+
+impl From<u64> for OwnedKey {
+ fn from(num: u64) -> Self {
+ Self::Num(num)
+ }
+}
+
+impl From<String> for OwnedKey {
+ fn from(string: String) -> Self {
+ Self::Str(string)
+ }
+}
+
+impl From<&'static str> for OwnedKey {
+ fn from(string: &'static str) -> Self {
+ Self::Str(string.to_string())
+ }
+}
+
+/// The borrowed variant of a table key.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum BorrowedKey<'a> {
+ Num(u64),
+ Str(&'a str),
+}
+
+impl From<u64> for BorrowedKey<'static> {
+ fn from(num: u64) -> Self {
+ Self::Num(num)
+ }
+}
+
+impl<'a> From<&'a String> for BorrowedKey<'a> {
+ fn from(string: &'a String) -> Self {
+ Self::Str(&string)
+ }
+}
+
+impl<'a> From<&'a str> for BorrowedKey<'a> {
+ fn from(string: &'a str) -> Self {
+ Self::Str(string)
+ }
+}
+
+/// An table entry which tracks key and value span.
+#[derive(Clone, PartialEq)]
+pub struct SpannedEntry<V> {
+ pub key: Span,
+ pub val: Spanned<V>,
+}
+
+impl<V> SpannedEntry<V> {
+ /// Create a new entry.
+ pub fn new(key: Span, val: Spanned<V>) -> Self {
+ Self { key, val }
+ }
+
+ /// Create an entry with the same span for key and value.
+ pub fn val(val: Spanned<V>) -> Self {
+ Self { key: Span::ZERO, val }
+ }
+
+ /// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>`
+ pub fn as_ref(&self) -> SpannedEntry<&V> {
+ SpannedEntry { key: self.key, val: self.val.as_ref() }
+ }
+
+ /// Map the entry to a different value type.
+ pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> {
+ SpannedEntry { key: self.key, val: self.val.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.fmt(f)?;
+ f.write_str(" ")?;
+ }
+ self.val.fmt(f)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Table;
+
+ #[test]
+ fn test_table_different_key_types_dont_interfere() {
+ let mut table = Table::new();
+ table.insert(10, "hello");
+ table.insert("twenty", "there");
+ assert_eq!(table.len(), 2);
+ assert_eq!(table[10], "hello");
+ assert_eq!(table["twenty"], "there");
+ }
+
+ #[test]
+ fn test_table_push_skips_already_inserted_keys() {
+ let mut table = Table::new();
+ table.insert(2, "2");
+ table.push("0");
+ table.insert(3, "3");
+ table.push("1");
+ table.push("4");
+ assert_eq!(table.len(), 5);
+ assert_eq!(table[0], "0");
+ assert_eq!(table[1], "1");
+ assert_eq!(table[2], "2");
+ assert_eq!(table[3], "3");
+ assert_eq!(table[4], "4");
+ }
+
+ #[test]
+ fn test_table_push_remove_push_reuses_index() {
+ let mut table = Table::new();
+ table.push("0");
+ table.push("1");
+ table.push("2");
+ table.remove(1);
+ table.push("a");
+ table.push("3");
+ assert_eq!(table.len(), 4);
+ assert_eq!(table[0], "0");
+ assert_eq!(table[1], "a");
+ assert_eq!(table[2], "2");
+ assert_eq!(table[3], "3");
+ }
+
+ #[test]
+ fn test_table_first_and_last_are_correct() {
+ let mut table = Table::new();
+ assert_eq!(table.first(), None);
+ assert_eq!(table.last(), None);
+ table.insert(4, "hi");
+ table.insert("string", "hi");
+ assert_eq!(table.first(), Some((4, &"hi")));
+ assert_eq!(table.last(), Some((4, &"hi")));
+ table.insert(2, "bye");
+ assert_eq!(table.first(), Some((2, &"bye")));
+ assert_eq!(table.last(), Some((4, &"hi")));
+ }
+
+ #[test]
+ fn test_table_format_debug() {
+ let mut table = Table::new();
+ assert_eq!(format!("{:?}", table), r#"()"#);
+ assert_eq!(format!("{:#?}", table), r#"()"#);
+
+ table.insert(10, "hello");
+ table.insert("twenty", "there");
+ table.insert("sp ace", "quotes");
+ assert_eq!(
+ format!("{:?}", table),
+ r#"(10="hello", "sp ace"="quotes", "twenty"="there")"#,
+ );
+ assert_eq!(format!("{:#?}", table).lines().collect::<Vec<_>>(), [
+ "(",
+ r#" 10 = "hello","#,
+ r#" "sp ace" = "quotes","#,
+ r#" "twenty" = "there","#,
+ ")",
+ ]);
+ }
+}
diff --git a/src/compute/value.rs b/src/compute/value.rs
new file mode 100644
index 00000000..daa3b17b
--- /dev/null
+++ b/src/compute/value.rs
@@ -0,0 +1,474 @@
+//! Computational values: Syntactical expressions can be evaluated into these.
+
+use std::fmt::{self, Debug, Formatter};
+use std::rc::Rc;
+
+use fontdock::{FontStyle, FontWeight, FontWidth};
+
+use crate::color::RgbaColor;
+use crate::layout::{Commands, Dir, LayoutContext, SpecAlign};
+use crate::length::{Length, ScaleLength};
+use crate::paper::Paper;
+use crate::syntax::span::{Span, Spanned};
+use crate::syntax::tree::SyntaxTree;
+use crate::syntax::Ident;
+use crate::{DynFuture, Feedback, Pass};
+use super::table::{BorrowedKey, SpannedEntry, Table};
+
+/// A computational value.
+#[derive(Clone)]
+pub enum Value {
+ /// An identifier: `ident`.
+ Ident(Ident),
+ /// A string: `"string"`.
+ Str(String),
+ /// A boolean: `true, false`.
+ Bool(bool),
+ /// A number: `1.2, 200%`.
+ Number(f64),
+ /// A length: `2cm, 5.2in`.
+ Length(Length),
+ /// A color value with alpha channel: `#f79143ff`.
+ Color(RgbaColor),
+ /// A table value: `(false, 12cm, greeting="hi")`.
+ Table(TableValue),
+ /// A syntax tree containing typesetting content.
+ Tree(SyntaxTree),
+ /// An executable function.
+ Func(FuncValue),
+ /// Layouting commands.
+ Commands(Commands),
+}
+
+impl Value {
+ /// A natural-language name of the type of this expression, e.g.
+ /// "identifier".
+ pub fn name(&self) -> &'static str {
+ use Value::*;
+ match self {
+ Ident(_) => "identifier",
+ Str(_) => "string",
+ Bool(_) => "bool",
+ Number(_) => "number",
+ Length(_) => "length",
+ Color(_) => "color",
+ Table(_) => "table",
+ Tree(_) => "syntax tree",
+ Func(_) => "function",
+ Commands(_) => "commands",
+ }
+ }
+}
+
+impl Debug for Value {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ use Value::*;
+ match self {
+ Ident(i) => i.fmt(f),
+ Str(s) => s.fmt(f),
+ Bool(b) => b.fmt(f),
+ Number(n) => n.fmt(f),
+ Length(s) => s.fmt(f),
+ Color(c) => c.fmt(f),
+ Table(t) => t.fmt(f),
+ Tree(t) => t.fmt(f),
+ Func(_) => f.pad("<function>"),
+ Commands(c) => c.fmt(f),
+ }
+ }
+}
+
+impl PartialEq for Value {
+ fn eq(&self, other: &Self) -> bool {
+ use Value::*;
+ match (self, other) {
+ (Ident(a), Ident(b)) => a == b,
+ (Str(a), Str(b)) => a == b,
+ (Bool(a), Bool(b)) => a == b,
+ (Number(a), Number(b)) => a == b,
+ (Length(a), Length(b)) => a == b,
+ (Color(a), Color(b)) => a == b,
+ (Table(a), Table(b)) => a == b,
+ (Tree(a), Tree(b)) => a == b,
+ (Func(a), Func(b)) => {
+ a.as_ref() as *const _ == b.as_ref() as *const _
+ }
+ (Commands(a), Commands(b)) => a == b,
+ _ => false,
+ }
+ }
+}
+
+/// An executable function value.
+///
+/// The first argument is a table containing the arguments passed to the
+/// function. The function may be asynchronous (as such it returns a dynamic
+/// future) and it may emit diagnostics, which are contained in the returned
+/// `Pass`. In the end, the function must evaluate to `Value`. Your typical
+/// typesetting function will return a `Commands` value which will instruct the
+/// layouting engine to do what the function pleases.
+///
+/// The dynamic function object is wrapped in an `Rc` to keep `Value` clonable.
+pub type FuncValue = Rc<
+ dyn Fn(TableValue, LayoutContext<'_>) -> DynFuture<Pass<Value>>
+>;
+
+/// A table of values.
+///
+/// # Example
+/// ```typst
+/// (false, 12cm, greeting="hi")
+/// ```
+pub type TableValue = Table<SpannedEntry<Value>>;
+
+impl TableValue {
+ /// Retrieve and remove the matching value with the lowest number key,
+ /// skipping and ignoring all non-matching entries with lower keys.
+ pub fn take<T: TryFromValue>(&mut self) -> Option<T> {
+ for (&key, entry) in self.nums() {
+ let expr = entry.val.as_ref();
+ if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
+ self.remove(key);
+ return Some(val);
+ }
+ }
+ None
+ }
+
+ /// Retrieve and remove the matching value with the lowest number key,
+ /// removing and generating errors for all non-matching entries with lower
+ /// keys.
+ pub fn expect<T: TryFromValue>(&mut self, f: &mut Feedback) -> Option<T> {
+ while let Some((num, _)) = self.first() {
+ let entry = self.remove(num).unwrap();
+ if let Some(val) = T::try_from_value(entry.val.as_ref(), f) {
+ return Some(val);
+ }
+ }
+ None
+ }
+
+ /// Retrieve and remove a matching value associated with the given key if
+ /// there is any.
+ ///
+ /// Generates an error if the key exists but the value does not match.
+ pub fn take_with_key<'a, K, T>(&mut self, key: K, f: &mut Feedback) -> Option<T>
+ where
+ K: Into<BorrowedKey<'a>>,
+ T: TryFromValue,
+ {
+ self.remove(key).and_then(|entry| {
+ let expr = entry.val.as_ref();
+ T::try_from_value(expr, f)
+ })
+ }
+
+ /// Retrieve and remove all matching pairs with number keys, skipping and
+ /// ignoring non-matching entries.
+ ///
+ /// The pairs are returned in order of increasing keys.
+ pub fn take_all_num<'a, T>(&'a mut self) -> impl Iterator<Item = (u64, T)> + 'a
+ where
+ T: TryFromValue,
+ {
+ let mut skip = 0;
+ std::iter::from_fn(move || {
+ for (&key, entry) in self.nums().skip(skip) {
+ let expr = entry.val.as_ref();
+ if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
+ self.remove(key);
+ return Some((key, val));
+ }
+ skip += 1;
+ }
+
+ None
+ })
+ }
+
+
+ /// Retrieve and remove all matching values with number keys, skipping and
+ /// ignoring non-matching entries.
+ ///
+ /// The values are returned in order of increasing keys.
+ pub fn take_all_num_vals<'a, T: 'a>(&'a mut self) -> impl Iterator<Item = T> + 'a
+ where
+ T: TryFromValue,
+ {
+ self.take_all_num::<T>().map(|(_, v)| v)
+ }
+
+ /// Retrieve and remove all matching pairs with string keys, skipping and
+ /// ignoring non-matching entries.
+ ///
+ /// The pairs are returned in order of increasing keys.
+ pub fn take_all_str<'a, T>(&'a mut self) -> impl Iterator<Item = (String, T)> + 'a
+ where
+ T: TryFromValue,
+ {
+ let mut skip = 0;
+ std::iter::from_fn(move || {
+ for (key, entry) in self.strs().skip(skip) {
+ let expr = entry.val.as_ref();
+ if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
+ let key = key.clone();
+ self.remove(&key);
+ return Some((key, val));
+ }
+ skip += 1;
+ }
+
+ None
+ })
+ }
+
+ /// Generated `"unexpected argument"` errors for all remaining entries.
+ pub fn unexpected(&self, f: &mut Feedback) {
+ for entry in self.values() {
+ let span = Span::merge(entry.key, entry.val.span);
+ error!(@f, span, "unexpected argument");
+ }
+ }
+}
+
+/// A trait for converting values into more specific types.
+pub trait TryFromValue: Sized {
+ // This trait takes references because we don't want to move the value
+ // out of its origin in case this returns `None`. This solution is not
+ // perfect because we need to do some cloning in the impls for this trait,
+ // but we haven't got a better solution, for now.
+
+ /// Try to convert a value to this type.
+ ///
+ /// Returns `None` and generates an appropriate error if the value is not
+ /// valid for this type.
+ fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self>;
+}
+
+macro_rules! impl_match {
+ ($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => {
+ impl TryFromValue for $type {
+ fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
+ #[allow(unreachable_patterns)]
+ match value.v {
+ $($p => Some($r)),*,
+ other => {
+ error!(
+ @f, value.span,
+ "expected {}, found {}", $name, other.name()
+ );
+ None
+ }
+ }
+ }
+ }
+ };
+}
+
+macro_rules! impl_ident {
+ ($type:ty, $name:expr, $parse:expr) => {
+ impl TryFromValue for $type {
+ fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
+ if let Value::Ident(ident) = value.v {
+ let val = $parse(ident.as_str());
+ if val.is_none() {
+ error!(@f, value.span, "invalid {}", $name);
+ }
+ val
+ } else {
+ error!(
+ @f, value.span,
+ "expected {}, found {}", $name, value.v.name()
+ );
+ None
+ }
+ }
+ }
+ };
+}
+
+impl<T: TryFromValue> TryFromValue for Spanned<T> {
+ fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
+ let span = value.span;
+ T::try_from_value(value, f).map(|v| Spanned { v, span })
+ }
+}
+
+impl_match!(Value, "value", v => v.clone());
+impl_match!(Ident, "identifier", Value::Ident(i) => i.clone());
+impl_match!(String, "string", Value::Str(s) => s.clone());
+impl_match!(bool, "bool", &Value::Bool(b) => b);
+impl_match!(f64, "number", &Value::Number(n) => n);
+impl_match!(Length, "length", &Value::Length(l) => l);
+impl_match!(SyntaxTree, "tree", Value::Tree(t) => t.clone());
+impl_match!(TableValue, "table", Value::Table(t) => t.clone());
+impl_match!(FuncValue, "function", Value::Func(f) => f.clone());
+impl_match!(ScaleLength, "number or length",
+ &Value::Length(length) => ScaleLength::Absolute(length),
+ &Value::Number(scale) => ScaleLength::Scaled(scale),
+);
+
+/// A value type that matches identifiers and strings and implements
+/// `Into<String>`.
+pub struct StringLike(pub String);
+
+impl From<StringLike> for String {
+ fn from(like: StringLike) -> String {
+ like.0
+ }
+}
+
+impl_match!(StringLike, "identifier or string",
+ Value::Ident(Ident(s)) => StringLike(s.clone()),
+ Value::Str(s) => StringLike(s.clone()),
+);
+
+impl_ident!(Dir, "direction", |s| match s {
+ "ltr" => Some(Self::LTR),
+ "rtl" => Some(Self::RTL),
+ "ttb" => Some(Self::TTB),
+ "btt" => Some(Self::BTT),
+ _ => None,
+});
+
+impl_ident!(SpecAlign, "alignment", |s| match s {
+ "left" => Some(Self::Left),
+ "right" => Some(Self::Right),
+ "top" => Some(Self::Top),
+ "bottom" => Some(Self::Bottom),
+ "center" => Some(Self::Center),
+ _ => None,
+});
+
+impl_ident!(FontStyle, "font style", FontStyle::from_name);
+impl_ident!(Paper, "paper", Paper::from_name);
+
+impl TryFromValue for FontWeight {
+ fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
+ match value.v {
+ &Value::Number(weight) => {
+ const MIN: u16 = 100;
+ const MAX: u16 = 900;
+
+ Some(Self(if weight < MIN as f64 {
+ error!(@f, value.span, "the minimum font weight is {}", MIN);
+ MIN
+ } else if weight > MAX as f64 {
+ error!(@f, value.span, "the maximum font weight is {}", MAX);
+ MAX
+ } else {
+ weight.round() as u16
+ }))
+ }
+ Value::Ident(ident) => {
+ let weight = Self::from_name(ident.as_str());
+ if weight.is_none() {
+ error!(@f, value.span, "invalid font weight");
+ }
+ weight
+ }
+ other => {
+ error!(
+ @f, value.span,
+ "expected font weight (name or number), found {}",
+ other.name(),
+ );
+ None
+ }
+ }
+ }
+}
+
+impl TryFromValue for FontWidth {
+ fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
+ match value.v {
+ &Value::Number(width) => {
+ const MIN: u16 = 1;
+ const MAX: u16 = 9;
+
+ Self::new(if width < MIN as f64 {
+ error!(@f, value.span, "the minimum font width is {}", MIN);
+ MIN
+ } else if width > MAX as f64 {
+ error!(@f, value.span, "the maximum font width is {}", MAX);
+ MAX
+ } else {
+ width.round() as u16
+ })
+ }
+ Value::Ident(ident) => {
+ let width = Self::from_name(ident.as_str());
+ if width.is_none() {
+ error!(@f, value.span, "invalid font width");
+ }
+ width
+ }
+ other => {
+ error!(
+ @f, value.span,
+ "expected font width (name or number), found {}",
+ other.name(),
+ );
+ None
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn entry(value: Value) -> SpannedEntry<Value> {
+ SpannedEntry::val(Spanned::zero(value))
+ }
+
+ #[test]
+ fn test_table_take_removes_correct_entry() {
+ let mut table = Table::new();
+ table.insert(1, entry(Value::Bool(false)));
+ table.insert(2, entry(Value::Str("hi".to_string())));
+ assert_eq!(table.take::<String>(), Some("hi".to_string()));
+ assert_eq!(table.len(), 1);
+ assert_eq!(table.take::<bool>(), Some(false));
+ assert!(table.is_empty());
+ }
+
+ #[test]
+ fn test_table_expect_errors_about_previous_entries() {
+ let mut f = Feedback::new();
+ let mut table = Table::new();
+ table.insert(1, entry(Value::Bool(false)));
+ table.insert(3, entry(Value::Str("hi".to_string())));
+ table.insert(5, entry(Value::Bool(true)));
+ assert_eq!(table.expect::<String>(&mut f), Some("hi".to_string()));
+ assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected string, found bool")]);
+ assert_eq!(table.len(), 1);
+ }
+
+ #[test]
+ fn test_table_take_with_key_removes_the_entry() {
+ let mut f = Feedback::new();
+ let mut table = Table::new();
+ table.insert(1, entry(Value::Bool(false)));
+ table.insert("hi", entry(Value::Bool(true)));
+ assert_eq!(table.take_with_key::<_, bool>(1, &mut f), Some(false));
+ assert_eq!(table.take_with_key::<_, f64>("hi", &mut f), None);
+ assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected number, found bool")]);
+ assert!(table.is_empty());
+ }
+
+ #[test]
+ fn test_table_take_all_removes_the_correct_entries() {
+ let mut table = Table::new();
+ table.insert(1, entry(Value::Bool(false)));
+ table.insert(3, entry(Value::Number(0.0)));
+ table.insert(7, entry(Value::Bool(true)));
+ assert_eq!(
+ table.take_all_num::<bool>().collect::<Vec<_>>(),
+ [(1, false), (7, true)],
+ );
+ assert_eq!(table.len(), 1);
+ assert_eq!(table[3].val.v, Value::Number(0.0));
+ }
+}