diff options
Diffstat (limited to 'src/eval')
| -rw-r--r-- | src/eval/args.rs | 193 | ||||
| -rw-r--r-- | src/eval/array.rs | 406 | ||||
| -rw-r--r-- | src/eval/cast.rs | 513 | ||||
| -rw-r--r-- | src/eval/dict.rs | 209 | ||||
| -rw-r--r-- | src/eval/func.rs | 574 | ||||
| -rw-r--r-- | src/eval/library.rs | 145 | ||||
| -rw-r--r-- | src/eval/methods.rs | 276 | ||||
| -rw-r--r-- | src/eval/mod.rs | 1554 | ||||
| -rw-r--r-- | src/eval/module.rs | 87 | ||||
| -rw-r--r-- | src/eval/ops.rs | 414 | ||||
| -rw-r--r-- | src/eval/scope.rs | 174 | ||||
| -rw-r--r-- | src/eval/str.rs | 514 | ||||
| -rw-r--r-- | src/eval/symbol.rs | 189 | ||||
| -rw-r--r-- | src/eval/value.rs | 497 |
14 files changed, 5745 insertions, 0 deletions
diff --git a/src/eval/args.rs b/src/eval/args.rs new file mode 100644 index 00000000..159e9a77 --- /dev/null +++ b/src/eval/args.rs @@ -0,0 +1,193 @@ +use std::fmt::{self, Debug, Formatter, Write}; + +use ecow::EcoVec; + +use super::{Array, Cast, Dict, Str, Value}; +use crate::diag::{bail, At, SourceResult}; +use crate::syntax::{Span, Spanned}; + +/// Evaluated arguments to a function. +#[derive(Clone, PartialEq, Hash)] +pub struct Args { + /// The span of the whole argument list. + pub span: Span, + /// The positional and named arguments. + pub items: EcoVec<Arg>, +} + +/// An argument to a function call: `12` or `draw: false`. +#[derive(Clone, PartialEq, Hash)] +pub struct Arg { + /// The span of the whole argument. + pub span: Span, + /// The name of the argument (`None` for positional arguments). + pub name: Option<Str>, + /// The value of the argument. + pub value: Spanned<Value>, +} + +impl Args { + /// Create positional arguments from a span and values. + pub fn new(span: Span, values: impl IntoIterator<Item = Value>) -> Self { + let items = values + .into_iter() + .map(|value| Arg { span, name: None, value: Spanned::new(value, span) }) + .collect(); + Self { span, items } + } + + /// Push a positional argument. + pub fn push(&mut self, span: Span, value: Value) { + self.items.push(Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }) + } + + /// Consume and cast the first positional argument if there is one. + pub fn eat<T>(&mut self) -> SourceResult<Option<T>> + where + T: Cast<Spanned<Value>>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() { + let value = self.items.remove(i).value; + let span = value.span; + return T::cast(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Consume and cast the first positional argument. + /// + /// Returns a `missing argument: {what}` error if no positional argument is + /// left. + pub fn expect<T>(&mut self, what: &str) -> SourceResult<T> + where + T: Cast<Spanned<Value>>, + { + match self.eat()? { + Some(v) => Ok(v), + None => bail!(self.span, "missing argument: {}", what), + } + } + + /// Find and consume the first castable positional argument. + pub fn find<T>(&mut self) -> SourceResult<Option<T>> + where + T: Cast<Spanned<Value>>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() && T::is(&slot.value) { + let value = self.items.remove(i).value; + let span = value.span; + return T::cast(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Find and consume all castable positional arguments. + pub fn all<T>(&mut self) -> SourceResult<Vec<T>> + where + T: Cast<Spanned<Value>>, + { + let mut list = vec![]; + while let Some(value) = self.find()? { + list.push(value); + } + Ok(list) + } + + /// Cast and remove the value for the given named argument, returning an + /// error if the conversion fails. + pub fn named<T>(&mut self, name: &str) -> SourceResult<Option<T>> + where + T: Cast<Spanned<Value>>, + { + // We don't quit once we have a match because when multiple matches + // exist, we want to remove all of them and use the last one. + let mut i = 0; + let mut found = None; + while i < self.items.len() { + if self.items[i].name.as_deref() == Some(name) { + let value = self.items.remove(i).value; + let span = value.span; + found = Some(T::cast(value).at(span)?); + } else { + i += 1; + } + } + Ok(found) + } + + /// Same as named, but with fallback to find. + pub fn named_or_find<T>(&mut self, name: &str) -> SourceResult<Option<T>> + where + T: Cast<Spanned<Value>>, + { + match self.named(name)? { + Some(value) => Ok(Some(value)), + None => self.find(), + } + } + + /// Take out all arguments into a new instance. + pub fn take(&mut self) -> Self { + Self { + span: self.span, + items: std::mem::take(&mut self.items), + } + } + + /// Return an "unexpected argument" error if there is any remaining + /// argument. + pub fn finish(self) -> SourceResult<()> { + if let Some(arg) = self.items.first() { + bail!(arg.span, "unexpected argument"); + } + Ok(()) + } + + /// Extract the positional arguments as an array. + pub fn to_pos(&self) -> Array { + self.items + .iter() + .filter(|item| item.name.is_none()) + .map(|item| item.value.v.clone()) + .collect() + } + + /// Extract the named arguments as a dictionary. + pub fn to_named(&self) -> Dict { + self.items + .iter() + .filter_map(|item| item.name.clone().map(|name| (name, item.value.v.clone()))) + .collect() + } +} + +impl Debug for Args { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char('(')?; + for (i, arg) in self.items.iter().enumerate() { + arg.fmt(f)?; + if i + 1 < self.items.len() { + f.write_str(", ")?; + } + } + f.write_char(')') + } +} + +impl Debug for Arg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(name) = &self.name { + f.write_str(name)?; + f.write_str(": ")?; + } + Debug::fmt(&self.value.v, f) + } +} diff --git a/src/eval/array.rs b/src/eval/array.rs new file mode 100644 index 00000000..53bae06f --- /dev/null +++ b/src/eval/array.rs @@ -0,0 +1,406 @@ +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter, Write}; +use std::ops::{Add, AddAssign}; + +use ecow::{eco_format, EcoString, EcoVec}; + +use super::{ops, Args, Func, Value, Vm}; +use crate::diag::{bail, At, SourceResult, StrResult}; + +/// Create a new [`Array`] from values. +#[macro_export] +#[doc(hidden)] +macro_rules! __array { + ($value:expr; $count:expr) => { + $crate::eval::Array::from_vec($crate::eval::eco_vec![$value.into(); $count]) + }; + + ($($value:expr),* $(,)?) => { + $crate::eval::Array::from_vec($crate::eval::eco_vec![$($value.into()),*]) + }; +} + +#[doc(inline)] +pub use crate::__array as array; +#[doc(hidden)] +pub use ecow::eco_vec; + +/// A reference counted array with value semantics. +#[derive(Default, Clone, PartialEq, Hash)] +pub struct Array(EcoVec<Value>); + +impl Array { + /// Create a new, empty array. + pub fn new() -> Self { + Self::default() + } + + /// Create a new array from an eco vector of values. + pub fn from_vec(vec: EcoVec<Value>) -> Self { + Self(vec) + } + + /// The length of the array. + pub fn len(&self) -> i64 { + self.0.len() as i64 + } + + /// The first value in the array. + pub fn first(&self) -> StrResult<&Value> { + self.0.first().ok_or_else(array_is_empty) + } + + /// Mutably borrow the first value in the array. + pub fn first_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().first_mut().ok_or_else(array_is_empty) + } + + /// The last value in the array. + pub fn last(&self) -> StrResult<&Value> { + self.0.last().ok_or_else(array_is_empty) + } + + /// Mutably borrow the last value in the array. + pub fn last_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().last_mut().ok_or_else(array_is_empty) + } + + /// Borrow the value at the given index. + pub fn at(&self, index: i64) -> StrResult<&Value> { + self.locate(index) + .and_then(|i| self.0.get(i)) + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Mutably borrow the value at the given index. + pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> { + let len = self.len(); + self.locate(index) + .and_then(move |i| self.0.make_mut().get_mut(i)) + .ok_or_else(|| out_of_bounds(index, len)) + } + + /// Push a value to the end of the array. + pub fn push(&mut self, value: Value) { + self.0.push(value); + } + + /// Remove the last value in the array. + pub fn pop(&mut self) -> StrResult<Value> { + self.0.pop().ok_or_else(array_is_empty) + } + + /// Insert a value at the specified index. + pub fn insert(&mut self, index: i64, value: Value) -> StrResult<()> { + let len = self.len(); + let i = self + .locate(index) + .filter(|&i| i <= self.0.len()) + .ok_or_else(|| out_of_bounds(index, len))?; + + self.0.insert(i, value); + Ok(()) + } + + /// Remove and return the value at the specified index. + pub fn remove(&mut self, index: i64) -> StrResult<Value> { + let len = self.len(); + let i = self + .locate(index) + .filter(|&i| i < self.0.len()) + .ok_or_else(|| out_of_bounds(index, len))?; + + Ok(self.0.remove(i)) + } + + /// Extract a contigous subregion of the array. + pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { + let len = self.len(); + let start = self + .locate(start) + .filter(|&start| start <= self.0.len()) + .ok_or_else(|| out_of_bounds(start, len))?; + + let end = end.unwrap_or(self.len()); + let end = self + .locate(end) + .filter(|&end| end <= self.0.len()) + .ok_or_else(|| out_of_bounds(end, len))? + .max(start); + + Ok(Self::from_vec(self.0[start..end].into())) + } + + /// Whether the array contains a specific value. + pub fn contains(&self, value: &Value) -> bool { + self.0.contains(value) + } + + /// Return the first matching element. + pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<Value>> { + if func.argc().map_or(false, |count| count != 1) { + bail!(func.span(), "function must have exactly one parameter"); + } + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if func.call(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(Some(item.clone())); + } + } + + Ok(None) + } + + /// Return the index of the first matching element. + pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<i64>> { + if func.argc().map_or(false, |count| count != 1) { + bail!(func.span(), "function must have exactly one parameter"); + } + for (i, item) in self.iter().enumerate() { + let args = Args::new(func.span(), [item.clone()]); + if func.call(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(Some(i as i64)); + } + } + + Ok(None) + } + + /// Return a new array with only those elements for which the function + /// returns true. + pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> { + if func.argc().map_or(false, |count| count != 1) { + bail!(func.span(), "function must have exactly one parameter"); + } + let mut kept = EcoVec::new(); + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if func.call(vm, args)?.cast::<bool>().at(func.span())? { + kept.push(item.clone()) + } + } + Ok(Self::from_vec(kept)) + } + + /// Transform each item in the array with a function. + pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> { + if func.argc().map_or(false, |count| !(1..=2).contains(&count)) { + bail!(func.span(), "function must have one or two parameters"); + } + let enumerate = func.argc() == Some(2); + self.iter() + .enumerate() + .map(|(i, item)| { + let mut args = Args::new(func.span(), []); + if enumerate { + args.push(func.span(), Value::Int(i as i64)); + } + args.push(func.span(), item.clone()); + func.call(vm, args) + }) + .collect() + } + + /// Fold all of the array's elements into one with a function. + pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult<Value> { + if func.argc().map_or(false, |count| count != 2) { + bail!(func.span(), "function must have exactly two parameters"); + } + let mut acc = init; + for item in self.iter() { + let args = Args::new(func.span(), [acc, item.clone()]); + acc = func.call(vm, args)?; + } + Ok(acc) + } + + /// Whether any element matches. + pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> { + if func.argc().map_or(false, |count| count != 1) { + bail!(func.span(), "function must have exactly one parameter"); + } + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if func.call(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether all elements match. + pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> { + if func.argc().map_or(false, |count| count != 1) { + bail!(func.span(), "function must have exactly one parameter"); + } + for item in self.iter() { + let args = Args::new(func.span(), [item.clone()]); + if !func.call(vm, args)?.cast::<bool>().at(func.span())? { + return Ok(false); + } + } + + Ok(true) + } + + /// Return a new array with all items from this and nested arrays. + pub fn flatten(&self) -> Self { + let mut flat = EcoVec::with_capacity(self.0.len()); + for item in self.iter() { + if let Value::Array(nested) = item { + flat.extend(nested.flatten().into_iter()); + } else { + flat.push(item.clone()); + } + } + Self::from_vec(flat) + } + + /// Returns a new array with reversed order. + pub fn rev(&self) -> Self { + self.0.iter().cloned().rev().collect() + } + + /// Join all values in the array, optionally with separator and last + /// separator (between the final two items). + pub fn join(&self, sep: Option<Value>, mut last: Option<Value>) -> StrResult<Value> { + let len = self.0.len(); + let sep = sep.unwrap_or(Value::None); + + let mut result = Value::None; + for (i, value) in self.iter().cloned().enumerate() { + if i > 0 { + if i + 1 == len && last.is_some() { + result = ops::join(result, last.take().unwrap())?; + } else { + result = ops::join(result, sep.clone())?; + } + } + + result = ops::join(result, value)?; + } + + Ok(result) + } + + /// Return a sorted version of this array. + /// + /// Returns an error if two values could not be compared. + pub fn sorted(&self) -> StrResult<Self> { + let mut result = Ok(()); + let mut vec = self.0.clone(); + vec.make_mut().sort_by(|a, b| { + a.partial_cmp(b).unwrap_or_else(|| { + if result.is_ok() { + result = Err(eco_format!( + "cannot order {} and {}", + a.type_name(), + b.type_name(), + )); + } + Ordering::Equal + }) + }); + result.map(|_| Self::from_vec(vec)) + } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: i64) -> StrResult<Self> { + let count = usize::try_from(n) + .ok() + .and_then(|n| self.0.len().checked_mul(n)) + .ok_or_else(|| format!("cannot repeat this array {} times", n))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } + + /// Extract a slice of the whole array. + pub fn as_slice(&self) -> &[Value] { + self.0.as_slice() + } + + /// Iterate over references to the contained values. + pub fn iter(&self) -> std::slice::Iter<Value> { + self.0.iter() + } + + /// Resolve an index. + fn locate(&self, index: i64) -> Option<usize> { + usize::try_from(if index >= 0 { index } else { self.len().checked_add(index)? }) + .ok() + } +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: i64) -> EcoString { + eco_format!("array index out of bounds (index: {}, len: {})", index, len) +} + +/// The error message when the array is empty. +#[cold] +fn array_is_empty() -> EcoString { + "array is empty".into() +} + +impl Debug for Array { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char('(')?; + for (i, value) in self.iter().enumerate() { + value.fmt(f)?; + if i + 1 < self.0.len() { + f.write_str(", ")?; + } + } + if self.len() == 1 { + f.write_char(',')?; + } + f.write_char(')') + } +} + +impl Add for Array { + type Output = Self; + + fn add(mut self, rhs: Array) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Array { + fn add_assign(&mut self, rhs: Array) { + self.0.extend(rhs.0); + } +} + +impl Extend<Value> for Array { + fn extend<T: IntoIterator<Item = Value>>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl FromIterator<Value> for Array { + fn from_iter<T: IntoIterator<Item = Value>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl IntoIterator for Array { + type Item = Value; + type IntoIter = ecow::vec::IntoIter<Value>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Array { + type Item = &'a Value; + type IntoIter = std::slice::Iter<'a, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} diff --git a/src/eval/cast.rs b/src/eval/cast.rs new file mode 100644 index 00000000..77521f7f --- /dev/null +++ b/src/eval/cast.rs @@ -0,0 +1,513 @@ +use std::num::NonZeroUsize; +use std::ops::Add; +use std::str::FromStr; + +use ecow::EcoString; + +use super::{castable, Array, Dict, Func, Regex, Str, Value}; +use crate::diag::StrResult; +use crate::doc::{Destination, Lang, Location, Region}; +use crate::font::{FontStretch, FontStyle, FontWeight}; +use crate::geom::{ + Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio, + Rel, Sides, Smart, +}; +use crate::model::{Content, Label, Selector, Transform}; +use crate::syntax::Spanned; + +/// Cast from a value to a specific type. +pub trait Cast<V = Value>: Sized { + /// Check whether the value is castable to `Self`. + fn is(value: &V) -> bool; + + /// Try to cast the value into an instance of `Self`. + fn cast(value: V) -> StrResult<Self>; + + /// Describe the acceptable values. + fn describe() -> CastInfo; + + /// Produce an error for an inacceptable value. + fn error(value: Value) -> StrResult<Self> { + Err(Self::describe().error(&value)) + } +} + +/// Describes a possible value for a cast. +#[derive(Debug, Clone, Hash)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(&'static str), + /// Multiple alternatives. + Union(Vec<Self>), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> EcoString { + fn accumulate( + info: &CastInfo, + found: &Value, + parts: &mut Vec<EcoString>, + matching_type: &mut bool, + ) { + match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr().into()); + if value.type_name() == found.type_name() { + *matching_type = true; + } + } + CastInfo::Type(ty) => parts.push((*ty).into()), + CastInfo::Union(options) => { + for option in options { + accumulate(option, found, parts, matching_type); + } + } + } + } + + let mut matching_type = false; + let mut parts = vec![]; + accumulate(self, found, &mut parts, &mut matching_type); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); + } + + crate::diag::comma_list(&mut msg, &parts, "or"); + + if !matching_type { + msg.push_str(", found "); + msg.push_str(found.type_name()); + } + + msg.into() + } +} + +impl Add for CastInfo { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + lhs.extend(rhs); + lhs + } + (Self::Union(mut lhs), rhs) => { + lhs.push(rhs); + lhs + } + (lhs, Self::Union(mut rhs)) => { + rhs.insert(0, lhs); + rhs + } + (lhs, rhs) => vec![lhs, rhs], + }) + } +} + +impl Cast for Value { + fn is(_: &Value) -> bool { + true + } + + fn cast(value: Value) -> StrResult<Self> { + Ok(value) + } + + fn describe() -> CastInfo { + CastInfo::Any + } +} + +impl<T: Cast> Cast<Spanned<Value>> for T { + fn is(value: &Spanned<Value>) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + T::cast(value.v) + } + + fn describe() -> CastInfo { + T::describe() + } +} + +impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> { + fn is(value: &Spanned<Value>) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned<Value>) -> StrResult<Self> { + let span = value.span; + T::cast(value.v).map(|t| Spanned::new(t, span)) + } + + fn describe() -> CastInfo { + T::describe() + } +} + +castable! { + Dir: "direction", +} + +castable! { + GenAlign: "alignment", +} + +castable! { + Regex: "regular expression", +} + +castable! { + Selector: "selector", + text: EcoString => Self::text(&text), + label: Label => Self::Label(label), + func: Func => func.select(None)?, + regex: Regex => Self::Regex(regex), +} + +castable! { + Axes<GenAlign>: "2d alignment", +} + +castable! { + PartialStroke: "stroke", + thickness: Length => Self { + paint: Smart::Auto, + thickness: Smart::Custom(thickness), + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + thickness: Smart::Auto, + }, +} + +castable! { + u32, + int: i64 => int.try_into().map_err(|_| { + if int < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, +} + +castable! { + usize, + int: i64 => int.try_into().map_err(|_| { + if int < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, +} + +castable! { + NonZeroUsize, + int: i64 => int + .try_into() + .and_then(|int: usize| int.try_into()) + .map_err(|_| if int <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} + +castable! { + Paint, + color: Color => Self::Solid(color), +} + +castable! { + char, + string: Str => { + let mut chars = string.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => c, + _ => Err("expected exactly one character")?, + } + }, +} + +castable! { + EcoString, + string: Str => string.into(), +} + +castable! { + String, + string: Str => string.into(), +} + +castable! { + Transform, + content: Content => Self::Content(content), + func: Func => { + if func.argc().map_or(false, |count| count != 1) { + Err("function must have exactly one parameter")? + } + Self::Func(func) + }, +} + +castable! { + Axes<Option<GenAlign>>, + align: GenAlign => { + let mut aligns = Axes::default(); + aligns.set(align.axis(), Some(align)); + aligns + }, + aligns: Axes<GenAlign> => aligns.map(Some), +} + +castable! { + Axes<Rel<Length>>, + array: Array => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} + +castable! { + Location, + mut dict: Dict => { + let page = dict.take("page")?.cast()?; + let x: Length = dict.take("x")?.cast()?; + let y: Length = dict.take("y")?.cast()?; + dict.finish(&["page", "x", "y"])?; + Self { page, pos: Point::new(x.abs, y.abs) } + }, +} + +castable! { + Destination, + loc: Location => Self::Internal(loc), + string: EcoString => Self::Url(string), +} + +castable! { + FontStyle, + /// The default, typically upright style. + "normal" => Self::Normal, + /// A cursive style with custom letterform. + "italic" => Self::Italic, + /// Just a slanted version of the normal style. + "oblique" => Self::Oblique, +} + +castable! { + FontWeight, + v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), + /// Thin weight (100). + "thin" => Self::THIN, + /// Extra light weight (200). + "extralight" => Self::EXTRALIGHT, + /// Light weight (300). + "light" => Self::LIGHT, + /// Regular weight (400). + "regular" => Self::REGULAR, + /// Medium weight (500). + "medium" => Self::MEDIUM, + /// Semibold weight (600). + "semibold" => Self::SEMIBOLD, + /// Bold weight (700). + "bold" => Self::BOLD, + /// Extrabold weight (800). + "extrabold" => Self::EXTRABOLD, + /// Black weight (900). + "black" => Self::BLACK, +} + +castable! { + FontStretch, + v: Ratio => Self::from_ratio(v.get() as f32), +} + +castable! { + Lang, + string: EcoString => Self::from_str(&string)?, +} + +castable! { + Region, + string: EcoString => Self::from_str(&string)?, +} + +/// Castable from [`Value::None`]. +pub struct NoneValue; + +impl Cast for NoneValue { + fn is(value: &Value) -> bool { + matches!(value, Value::None) + } + + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(Self), + _ => <Self as Cast>::error(value), + } + } + + fn describe() -> CastInfo { + CastInfo::Type("none") + } +} + +impl<T: Cast> Cast for Option<T> { + fn is(value: &Value) -> bool { + matches!(value, Value::None) || T::is(value) + } + + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(None), + v if T::is(&v) => Ok(Some(T::cast(v)?)), + _ => <Self as Cast>::error(value), + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("none") + } +} + +/// Castable from [`Value::Auto`]. +pub struct AutoValue; + +impl Cast for AutoValue { + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) + } + + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => <Self as Cast>::error(value), + } + } + + fn describe() -> CastInfo { + CastInfo::Type("auto") + } +} + +impl<T: Cast> Cast for Smart<T> { + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) || T::is(value) + } + + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self::Auto), + v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), + _ => <Self as Cast>::error(value), + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("auto") + } +} + +impl<T> Cast for Sides<Option<T>> +where + T: Cast + Copy, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Dict(_)) || T::is(value) + } + + fn cast(mut value: Value) -> StrResult<Self> { + if let Value::Dict(dict) = &mut value { + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + + let rest = take("rest")?; + let x = take("x")?.or(rest); + let y = take("y")?.or(rest); + let sides = Sides { + left: take("left")?.or(x), + top: take("top")?.or(y), + right: take("right")?.or(x), + bottom: take("bottom")?.or(y), + }; + + dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + + Ok(sides) + } else if T::is(&value) { + Ok(Self::splat(Some(T::cast(value)?))) + } else { + <Self as Cast>::error(value) + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } +} + +impl<T> Cast for Corners<Option<T>> +where + T: Cast + Copy, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Dict(_)) || T::is(value) + } + + fn cast(mut value: Value) -> StrResult<Self> { + if let Value::Dict(dict) = &mut value { + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + + let rest = take("rest")?; + let left = take("left")?.or(rest); + let top = take("top")?.or(rest); + let right = take("right")?.or(rest); + let bottom = take("bottom")?.or(rest); + let corners = Corners { + top_left: take("top-left")?.or(top).or(left), + top_right: take("top-right")?.or(top).or(right), + bottom_right: take("bottom-right")?.or(bottom).or(right), + bottom_left: take("bottom-left")?.or(bottom).or(left), + }; + + dict.finish(&[ + "top-left", + "top-right", + "bottom-right", + "bottom-left", + "left", + "top", + "right", + "bottom", + "rest", + ])?; + + Ok(corners) + } else if T::is(&value) { + Ok(Self::splat(Some(T::cast(value)?))) + } else { + <Self as Cast>::error(value) + } + } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } +} diff --git a/src/eval/dict.rs b/src/eval/dict.rs new file mode 100644 index 00000000..6c1934c9 --- /dev/null +++ b/src/eval/dict.rs @@ -0,0 +1,209 @@ +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Formatter, Write}; +use std::ops::{Add, AddAssign}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; + +use super::{array, Array, Str, Value}; +use crate::diag::StrResult; +use crate::syntax::is_ident; +use crate::util::ArcExt; + +/// Create a new [`Dict`] from key-value pairs. +#[macro_export] +#[doc(hidden)] +macro_rules! __dict { + ($($key:expr => $value:expr),* $(,)?) => {{ + #[allow(unused_mut)] + let mut map = std::collections::BTreeMap::new(); + $(map.insert($key.into(), $value.into());)* + $crate::eval::Dict::from_map(map) + }}; +} + +#[doc(inline)] +pub use crate::__dict as dict; + +/// A reference-counted dictionary with value semantics. +#[derive(Default, Clone, PartialEq, Hash)] +pub struct Dict(Arc<BTreeMap<Str, Value>>); + +impl Dict { + /// Create a new, empty dictionary. + pub fn new() -> Self { + Self::default() + } + + /// Create a new dictionary from a mapping of strings to values. + pub fn from_map(map: BTreeMap<Str, Value>) -> Self { + Self(Arc::new(map)) + } + + /// Whether the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// The number of pairs in the dictionary. + pub fn len(&self) -> i64 { + self.0.len() as i64 + } + + /// Borrow the value the given `key` maps to. + pub fn at(&self, key: &str) -> StrResult<&Value> { + self.0.get(key).ok_or_else(|| missing_key(key)) + } + + /// Mutably borrow the value the given `key` maps to. + pub fn at_mut(&mut self, key: &str) -> StrResult<&mut Value> { + Arc::make_mut(&mut self.0) + .get_mut(key) + .ok_or_else(|| missing_key(key)) + } + + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> StrResult<Value> { + Arc::make_mut(&mut self.0) + .remove(key) + .ok_or_else(|| eco_format!("missing key: {:?}", Str::from(key))) + } + + /// Whether the dictionary contains a specific key. + pub fn contains(&self, key: &str) -> bool { + self.0.contains_key(key) + } + + /// Insert a mapping from the given `key` to the given `value`. + pub fn insert(&mut self, key: Str, value: Value) { + Arc::make_mut(&mut self.0).insert(key, value); + } + + /// Remove a mapping by `key` and return the value. + pub fn remove(&mut self, key: &str) -> StrResult<Value> { + match Arc::make_mut(&mut self.0).remove(key) { + Some(value) => Ok(value), + None => Err(missing_key(key)), + } + } + + /// Clear the dictionary. + pub fn clear(&mut self) { + if Arc::strong_count(&self.0) == 1 { + Arc::make_mut(&mut self.0).clear(); + } else { + *self = Self::new(); + } + } + + /// Return the keys of the dictionary as an array. + pub fn keys(&self) -> Array { + self.0.keys().cloned().map(Value::Str).collect() + } + + /// Return the values of the dictionary as an array. + pub fn values(&self) -> Array { + self.0.values().cloned().collect() + } + + /// Return the values of the dictionary as an array of pairs (arrays of + /// length two). + pub fn pairs(&self) -> Array { + self.0 + .iter() + .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) + .collect() + } + + /// Iterate over pairs of references to the contained keys and values. + pub fn iter(&self) -> std::collections::btree_map::Iter<Str, Value> { + self.0.iter() + } + + /// Return an "unexpected key" error if there is any remaining pair. + pub fn finish(&self, expected: &[&str]) -> StrResult<()> { + if let Some((key, _)) = self.iter().next() { + let parts: Vec<_> = expected.iter().map(|s| eco_format!("\"{s}\"")).collect(); + let mut msg = format!("unexpected key {key:?}, valid keys are "); + crate::diag::comma_list(&mut msg, &parts, "and"); + return Err(msg.into()); + } + Ok(()) + } +} + +/// The missing key access error message. +#[cold] +fn missing_key(key: &str) -> EcoString { + eco_format!("dictionary does not contain key {:?}", Str::from(key)) +} + +impl Debug for Dict { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char('(')?; + if self.is_empty() { + f.write_char(':')?; + } + for (i, (key, value)) in self.iter().enumerate() { + if is_ident(key) { + f.write_str(key)?; + } else { + write!(f, "{key:?}")?; + } + f.write_str(": ")?; + value.fmt(f)?; + if i + 1 < self.0.len() { + f.write_str(", ")?; + } + } + f.write_char(')') + } +} + +impl Add for Dict { + type Output = Self; + + fn add(mut self, rhs: Dict) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Dict { + fn add_assign(&mut self, rhs: Dict) { + match Arc::try_unwrap(rhs.0) { + Ok(map) => self.extend(map), + Err(rc) => self.extend(rc.iter().map(|(k, v)| (k.clone(), v.clone()))), + } + } +} + +impl Extend<(Str, Value)> for Dict { + fn extend<T: IntoIterator<Item = (Str, Value)>>(&mut self, iter: T) { + Arc::make_mut(&mut self.0).extend(iter); + } +} + +impl FromIterator<(Str, Value)> for Dict { + fn from_iter<T: IntoIterator<Item = (Str, Value)>>(iter: T) -> Self { + Self(Arc::new(iter.into_iter().collect())) + } +} + +impl IntoIterator for Dict { + type Item = (Str, Value); + type IntoIter = std::collections::btree_map::IntoIter<Str, Value>; + + fn into_iter(self) -> Self::IntoIter { + Arc::take(self.0).into_iter() + } +} + +impl<'a> IntoIterator for &'a Dict { + type Item = (&'a Str, &'a Value); + type IntoIter = std::collections::btree_map::Iter<'a, Str, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} diff --git a/src/eval/func.rs b/src/eval/func.rs new file mode 100644 index 00000000..e5280932 --- /dev/null +++ b/src/eval/func.rs @@ -0,0 +1,574 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use comemo::{Prehashed, Track, Tracked, TrackedMut}; +use ecow::EcoString; + +use super::{Args, CastInfo, Dict, Eval, Flow, Route, Scope, Scopes, Tracer, Value, Vm}; +use crate::diag::{bail, SourceResult, StrResult}; +use crate::model::{Node, NodeId, Selector, StyleMap}; +use crate::syntax::ast::{self, AstNode, Expr}; +use crate::syntax::{SourceId, Span, SyntaxNode}; +use crate::util::hash128; +use crate::World; + +/// An evaluatable function. +#[derive(Clone, Hash)] +pub struct Func(Arc<Prehashed<Repr>>, Span); + +/// The different kinds of function representations. +#[derive(Hash)] +enum Repr { + /// A native rust function. + Native(Native), + /// A user-defined closure. + Closure(Closure), + /// A nested function with pre-applied arguments. + With(Func, Args), +} + +impl Func { + /// Create a new function from a type that can be turned into a function. + pub fn from_type<T: FuncType>(name: &'static str) -> Self { + T::create_func(name) + } + + /// Create a new function from a native rust function. + pub fn from_fn( + func: fn(&Vm, &mut Args) -> SourceResult<Value>, + info: FuncInfo, + ) -> Self { + Self( + Arc::new(Prehashed::new(Repr::Native(Native { + func, + set: None, + node: None, + info, + }))), + Span::detached(), + ) + } + + /// Create a new function from a native rust node. + pub fn from_node<T: Node>(mut info: FuncInfo) -> Self { + info.params.extend(T::properties()); + Self( + Arc::new(Prehashed::new(Repr::Native(Native { + func: |ctx, args| { + let styles = T::set(args, true)?; + let content = T::construct(ctx, args)?; + Ok(Value::Content(content.styled_with_map(styles.scoped()))) + }, + set: Some(|args| T::set(args, false)), + node: Some(NodeId::of::<T>()), + info, + }))), + Span::detached(), + ) + } + + /// Create a new function from a closure. + pub(super) fn from_closure(closure: Closure, span: Span) -> Self { + Self(Arc::new(Prehashed::new(Repr::Closure(closure))), span) + } + + /// The name of the function. + pub fn name(&self) -> Option<&str> { + match &**self.0 { + Repr::Native(native) => Some(native.info.name), + Repr::Closure(closure) => closure.name.as_deref(), + Repr::With(func, _) => func.name(), + } + } + + /// Extract details the function. + pub fn info(&self) -> Option<&FuncInfo> { + match &**self.0 { + Repr::Native(native) => Some(&native.info), + Repr::With(func, _) => func.info(), + _ => None, + } + } + + /// The function's span. + pub fn span(&self) -> Span { + self.1 + } + + /// Attach a span to the function. + pub fn spanned(mut self, span: Span) -> Self { + self.1 = span; + self + } + + /// The number of positional arguments this function takes, if known. + pub fn argc(&self) -> Option<usize> { + match &**self.0 { + Repr::Closure(closure) => closure.argc(), + Repr::With(wrapped, applied) => Some(wrapped.argc()?.saturating_sub( + applied.items.iter().filter(|arg| arg.name.is_none()).count(), + )), + _ => None, + } + } + + /// Call the function with the given arguments. + pub fn call(&self, vm: &mut Vm, mut args: Args) -> SourceResult<Value> { + match &**self.0 { + Repr::Native(native) => { + let value = (native.func)(vm, &mut args)?; + args.finish()?; + Ok(value) + } + Repr::Closure(closure) => { + // Determine the route inside the closure. + let fresh = Route::new(closure.location); + let route = + if vm.location.is_detached() { fresh.track() } else { vm.route }; + + Closure::call( + self, + vm.world, + route, + TrackedMut::reborrow_mut(&mut vm.tracer), + vm.depth + 1, + args, + ) + } + Repr::With(wrapped, applied) => { + args.items = applied.items.iter().cloned().chain(args.items).collect(); + return wrapped.call(vm, args); + } + } + } + + /// Call the function without an existing virtual machine. + pub fn call_detached( + &self, + world: Tracked<dyn World>, + args: Args, + ) -> SourceResult<Value> { + let route = Route::default(); + let id = SourceId::detached(); + let scopes = Scopes::new(None); + let mut tracer = Tracer::default(); + let mut vm = Vm::new(world, route.track(), tracer.track_mut(), id, scopes, 0); + self.call(&mut vm, args) + } + + /// Apply the given arguments to the function. + pub fn with(self, args: Args) -> Self { + let span = self.1; + Self(Arc::new(Prehashed::new(Repr::With(self, args))), span) + } + + /// Create a selector for this function's node type, filtering by node's + /// whose [fields](super::Content::field) match the given arguments. + pub fn where_(self, args: &mut Args) -> StrResult<Selector> { + let fields = args.to_named(); + args.items.retain(|arg| arg.name.is_none()); + self.select(Some(fields)) + } + + /// Execute the function's set rule and return the resulting style map. + pub fn set(&self, mut args: Args) -> SourceResult<StyleMap> { + Ok(match &**self.0 { + Repr::Native(Native { set: Some(set), .. }) => { + let styles = set(&mut args)?; + args.finish()?; + styles + } + _ => StyleMap::new(), + }) + } + + /// Create a selector for this function's node type. + pub fn select(&self, fields: Option<Dict>) -> StrResult<Selector> { + match **self.0 { + Repr::Native(Native { node: Some(id), .. }) => { + if id == item!(text_id) { + Err("to select text, please use a string or regex instead")?; + } + + Ok(Selector::Node(id, fields)) + } + _ => Err("this function is not selectable")?, + } + } +} + +impl Debug for Func { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self.name() { + Some(name) => write!(f, "<function {name}>"), + None => f.write_str("<function>"), + } + } +} + +impl PartialEq for Func { + fn eq(&self, other: &Self) -> bool { + hash128(&self.0) == hash128(&other.0) + } +} + +/// Types that can be turned into functions. +pub trait FuncType { + /// Create a function with the given name from this type. + fn create_func(name: &'static str) -> Func; +} + +/// A function defined by a native rust function or node. +struct Native { + /// The function pointer. + func: fn(&Vm, &mut Args) -> SourceResult<Value>, + /// The set rule. + set: Option<fn(&mut Args) -> SourceResult<StyleMap>>, + /// The id of the node to customize with this function's show rule. + node: Option<NodeId>, + /// Documentation of the function. + info: FuncInfo, +} + +impl Hash for Native { + fn hash<H: Hasher>(&self, state: &mut H) { + (self.func as usize).hash(state); + self.set.map(|set| set as usize).hash(state); + self.node.hash(state); + } +} + +/// Details about a function. +#[derive(Debug, Clone)] +pub struct FuncInfo { + /// The function's name. + pub name: &'static str, + /// The display name of the function. + pub display: &'static str, + /// Documentation for the function. + pub docs: &'static str, + /// Details about the function's parameters. + pub params: Vec<ParamInfo>, + /// Valid types for the return value. + pub returns: Vec<&'static str>, + /// Which category the function is part of. + pub category: &'static str, +} + +impl FuncInfo { + /// Get the parameter info for a parameter with the given name + pub fn param(&self, name: &str) -> Option<&ParamInfo> { + self.params.iter().find(|param| param.name == name) + } +} + +/// Describes a named parameter. +#[derive(Debug, Clone)] +pub struct ParamInfo { + /// The parameter's name. + pub name: &'static str, + /// Documentation for the parameter. + pub docs: &'static str, + /// Valid values for the parameter. + pub cast: CastInfo, + /// Is the parameter positional? + pub positional: bool, + /// Is the parameter named? + /// + /// Can be true even if `positional` is true if the parameter can be given + /// in both variants. + pub named: bool, + /// Is the parameter required? + pub required: bool, + /// Can the parameter be given any number of times? + pub variadic: bool, + /// Is the parameter settable with a set rule? + pub settable: bool, +} + +/// A user-defined closure. +#[derive(Hash)] +pub(super) struct Closure { + /// The source file where the closure was defined. + pub location: SourceId, + /// The name of the closure. + pub name: Option<EcoString>, + /// Captured values from outer scopes. + pub captured: Scope, + /// The parameter names and default values. Parameters with default value + /// are named parameters. + pub params: Vec<(EcoString, Option<Value>)>, + /// The name of an argument sink where remaining arguments are placed. + pub sink: Option<EcoString>, + /// The expression the closure should evaluate to. + pub body: Expr, +} + +impl Closure { + /// Call the function in the context with the arguments. + #[comemo::memoize] + fn call( + this: &Func, + world: Tracked<dyn World>, + route: Tracked<Route>, + tracer: TrackedMut<Tracer>, + depth: usize, + mut args: Args, + ) -> SourceResult<Value> { + let closure = match &**this.0 { + Repr::Closure(closure) => closure, + _ => panic!("`this` must be a closure"), + }; + + // Don't leak the scopes from the call site. Instead, we use the scope + // of captured variables we collected earlier. + let mut scopes = Scopes::new(None); + scopes.top = closure.captured.clone(); + + // Provide the closure itself for recursive calls. + if let Some(name) = &closure.name { + scopes.top.define(name.clone(), Value::Func(this.clone())); + } + + // Parse the arguments according to the parameter list. + for (param, default) in &closure.params { + scopes.top.define( + param.clone(), + match default { + Some(default) => { + args.named::<Value>(param)?.unwrap_or_else(|| default.clone()) + } + None => args.expect::<Value>(param)?, + }, + ); + } + + // Put the remaining arguments into the sink. + if let Some(sink) = &closure.sink { + scopes.top.define(sink.clone(), args.take()); + } + + // Ensure all arguments have been used. + args.finish()?; + + // Evaluate the body. + let mut sub = Vm::new(world, route, tracer, closure.location, scopes, depth); + let result = closure.body.eval(&mut sub); + + // Handle control flow. + match sub.flow { + Some(Flow::Return(_, Some(explicit))) => return Ok(explicit), + Some(Flow::Return(_, None)) => {} + Some(flow) => bail!(flow.forbidden()), + None => {} + } + + result + } + + /// The number of positional arguments this function takes, if known. + fn argc(&self) -> Option<usize> { + if self.sink.is_some() { + return None; + } + + Some(self.params.iter().filter(|(_, default)| default.is_none()).count()) + } +} + +/// A visitor that determines which variables to capture for a closure. +pub(super) struct CapturesVisitor<'a> { + external: &'a Scopes<'a>, + internal: Scopes<'a>, + captures: Scope, +} + +impl<'a> CapturesVisitor<'a> { + /// Create a new visitor for the given external scopes. + pub fn new(external: &'a Scopes) -> Self { + Self { + external, + internal: Scopes::new(None), + captures: Scope::new(), + } + } + + /// Return the scope of captured variables. + pub fn finish(self) -> Scope { + self.captures + } + + /// Visit any node and collect all captured variables. + pub fn visit(&mut self, node: &SyntaxNode) { + match node.cast() { + // Every identifier is a potential variable that we need to capture. + // Identifiers that shouldn't count as captures because they + // actually bind a new name are handled below (individually through + // the expressions that contain them). + Some(ast::Expr::Ident(ident)) => self.capture(ident), + Some(ast::Expr::MathIdent(ident)) => self.capture_in_math(ident), + + // Code and content blocks create a scope. + Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { + self.internal.enter(); + for child in node.children() { + self.visit(child); + } + self.internal.exit(); + } + + // A closure contains parameter bindings, which are bound before the + // body is evaluated. Care must be taken so that the default values + // of named parameters cannot access previous parameter bindings. + Some(ast::Expr::Closure(expr)) => { + for param in expr.params() { + if let ast::Param::Named(named) = param { + self.visit(named.expr().as_untyped()); + } + } + + self.internal.enter(); + if let Some(name) = expr.name() { + self.bind(name); + } + + for param in expr.params() { + match param { + ast::Param::Pos(ident) => self.bind(ident), + ast::Param::Named(named) => self.bind(named.name()), + ast::Param::Sink(ident) => self.bind(ident), + } + } + + self.visit(expr.body().as_untyped()); + self.internal.exit(); + } + + // A let expression contains a binding, but that binding is only + // active after the body is evaluated. + Some(ast::Expr::Let(expr)) => { + if let Some(init) = expr.init() { + self.visit(init.as_untyped()); + } + self.bind(expr.binding()); + } + + // A for loop contains one or two bindings in its pattern. These are + // active after the iterable is evaluated but before the body is + // evaluated. + Some(ast::Expr::For(expr)) => { + self.visit(expr.iter().as_untyped()); + self.internal.enter(); + let pattern = expr.pattern(); + if let Some(key) = pattern.key() { + self.bind(key); + } + self.bind(pattern.value()); + self.visit(expr.body().as_untyped()); + self.internal.exit(); + } + + // An import contains items, but these are active only after the + // path is evaluated. + Some(ast::Expr::Import(expr)) => { + self.visit(expr.source().as_untyped()); + if let Some(ast::Imports::Items(items)) = expr.imports() { + for item in items { + self.bind(item); + } + } + } + + // Everything else is traversed from left to right. + _ => { + for child in node.children() { + self.visit(child); + } + } + } + } + + /// Bind a new internal variable. + fn bind(&mut self, ident: ast::Ident) { + self.internal.top.define(ident.take(), Value::None); + } + + /// Capture a variable if it isn't internal. + fn capture(&mut self, ident: ast::Ident) { + if self.internal.get(&ident).is_err() { + if let Ok(value) = self.external.get(&ident) { + self.captures.define_captured(ident.take(), value.clone()); + } + } + } + + /// Capture a variable in math mode if it isn't internal. + fn capture_in_math(&mut self, ident: ast::MathIdent) { + if self.internal.get(&ident).is_err() { + if let Ok(value) = self.external.get_in_math(&ident) { + self.captures.define_captured(ident.take(), value.clone()); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syntax::parse; + + #[track_caller] + fn test(text: &str, result: &[&str]) { + let mut scopes = Scopes::new(None); + scopes.top.define("f", 0); + scopes.top.define("x", 0); + scopes.top.define("y", 0); + scopes.top.define("z", 0); + + let mut visitor = CapturesVisitor::new(&scopes); + let root = parse(text); + visitor.visit(&root); + + let captures = visitor.finish(); + let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); + names.sort(); + + assert_eq!(names, result); + } + + #[test] + fn test_captures() { + // Let binding and function definition. + test("#let x = x", &["x"]); + test("#let x; #(x + y)", &["y"]); + test("#let f(x, y) = x + y", &[]); + test("#let f(x, y) = f", &[]); + test("#let f = (x, y) => f", &["f"]); + + // Closure with different kinds of params. + test("#((x, y) => x + z)", &["z"]); + test("#((x: y, z) => x + z)", &["y"]); + test("#((..x) => x + y)", &["y"]); + test("#((x, y: x + z) => x + y)", &["x", "z"]); + test("#{x => x; x}", &["x"]); + + // Show rule. + test("#show y: x => x", &["y"]); + test("#show y: x => x + z", &["y", "z"]); + test("#show x: x => x", &["x"]); + + // For loop. + test("#for x in y { x + z }", &["y", "z"]); + test("#for x, y in y { x + y }", &["y"]); + test("#for x in y {} #x", &["x", "y"]); + + // Import. + test("#import z: x, y", &["z"]); + test("#import x + y: x, y, z", &["x", "y"]); + + // Blocks. + test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); + test("#[#let x = 1]#x", &["x"]); + } +} diff --git a/src/eval/library.rs b/src/eval/library.rs new file mode 100644 index 00000000..adfcc6e7 --- /dev/null +++ b/src/eval/library.rs @@ -0,0 +1,145 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; + +use ecow::EcoString; +use once_cell::sync::OnceCell; + +use super::Module; +use crate::diag::SourceResult; +use crate::doc::Document; +use crate::geom::{Abs, Dir}; +use crate::model::{Content, NodeId, StyleChain, StyleMap, Vt}; +use crate::util::hash128; + +/// Definition of Typst's standard library. +#[derive(Debug, Clone, Hash)] +pub struct Library { + /// The scope containing definitions that are available everywhere. + pub global: Module, + /// The scope containing definitions available in math mode. + pub math: Module, + /// The default properties for page size, font selection and so on. + pub styles: StyleMap, + /// Defines which standard library items fulfill which syntactical roles. + pub items: LangItems, +} + +/// Definition of library items the language is aware of. +#[derive(Clone)] +pub struct LangItems { + /// The root layout function. + pub layout: + fn(vt: &mut Vt, content: &Content, styles: StyleChain) -> SourceResult<Document>, + /// Access the em size. + pub em: fn(StyleChain) -> Abs, + /// Access the text direction. + pub dir: fn(StyleChain) -> Dir, + /// Whitespace. + pub space: fn() -> Content, + /// A forced line break: `\`. + pub linebreak: fn() -> Content, + /// Plain text without markup. + pub text: fn(text: EcoString) -> Content, + /// The id of the text node. + pub text_id: NodeId, + /// Get the string if this is a text node. + pub text_str: fn(&Content) -> Option<&str>, + /// A smart quote: `'` or `"`. + pub smart_quote: fn(double: bool) -> Content, + /// A paragraph break. + pub parbreak: fn() -> Content, + /// Strong content: `*Strong*`. + pub strong: fn(body: Content) -> Content, + /// Emphasized content: `_Emphasized_`. + pub emph: fn(body: Content) -> Content, + /// Raw text with optional syntax highlighting: `` `...` ``. + pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content, + /// A hyperlink: `https://typst.org`. + pub link: fn(url: EcoString) -> Content, + /// A reference: `@target`. + pub ref_: fn(target: EcoString) -> Content, + /// A section heading: `= Introduction`. + pub heading: fn(level: NonZeroUsize, body: Content) -> Content, + /// An item in a bullet list: `- ...`. + pub list_item: fn(body: Content) -> Content, + /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. + pub enum_item: fn(number: Option<NonZeroUsize>, body: Content) -> Content, + /// An item in a term list: `/ Term: Details`. + pub term_item: fn(term: Content, description: Content) -> Content, + /// A mathematical formula: `$x$`, `$ x^2 $`. + pub formula: fn(body: Content, block: bool) -> Content, + /// An alignment point in a formula: `&`. + pub math_align_point: fn() -> Content, + /// Matched delimiters surrounding math in a formula: `[x + y]`. + pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content, + /// A base with optional attachments in a formula: `a_1^2`. + pub math_attach: + fn(base: Content, bottom: Option<Content>, top: Option<Content>) -> Content, + /// A base with an accent: `arrow(x)`. + pub math_accent: fn(base: Content, accent: char) -> Content, + /// A fraction in a formula: `x/2`. + pub math_frac: fn(num: Content, denom: Content) -> Content, +} + +impl Debug for LangItems { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("LangItems { .. }") + } +} + +impl Hash for LangItems { + fn hash<H: Hasher>(&self, state: &mut H) { + (self.layout as usize).hash(state); + (self.em as usize).hash(state); + (self.dir as usize).hash(state); + self.space.hash(state); + self.linebreak.hash(state); + self.text.hash(state); + self.text_id.hash(state); + (self.text_str as usize).hash(state); + self.smart_quote.hash(state); + self.parbreak.hash(state); + self.strong.hash(state); + self.emph.hash(state); + self.raw.hash(state); + self.link.hash(state); + self.ref_.hash(state); + self.heading.hash(state); + self.list_item.hash(state); + self.enum_item.hash(state); + self.term_item.hash(state); + self.formula.hash(state); + self.math_align_point.hash(state); + self.math_delimited.hash(state); + self.math_attach.hash(state); + self.math_accent.hash(state); + self.math_frac.hash(state); + } +} + +/// Global storage for lang items. +#[doc(hidden)] +pub static LANG_ITEMS: OnceCell<LangItems> = OnceCell::new(); + +/// Set the lang items. This is a hack :( +/// +/// Passing the lang items everywhere they are needed (especially the text node +/// related things) is very painful. By storing them globally, in theory, we +/// break incremental, but only when different sets of lang items are used in +/// the same program. For this reason, if this function is called multiple +/// times, the items must be the same. +pub fn set_lang_items(items: LangItems) { + if let Err(items) = LANG_ITEMS.set(items) { + let first = hash128(LANG_ITEMS.get().unwrap()); + let second = hash128(&items); + assert_eq!(first, second, "set differing lang items"); + } +} + +/// Access a lang item. +macro_rules! item { + ($name:ident) => { + $crate::eval::LANG_ITEMS.get().unwrap().$name + }; +} diff --git a/src/eval/methods.rs b/src/eval/methods.rs new file mode 100644 index 00000000..dcb1ca31 --- /dev/null +++ b/src/eval/methods.rs @@ -0,0 +1,276 @@ +//! Methods on values. + +use ecow::EcoString; + +use super::{Args, Str, Value, Vm}; +use crate::diag::{At, SourceResult}; +use crate::syntax::Span; + +/// Call a method on a value. +pub fn call( + vm: &mut Vm, + value: Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + let output = match value { + Value::Color(color) => match method { + "lighten" => Value::Color(color.lighten(args.expect("amount")?)), + "darken" => Value::Color(color.darken(args.expect("amount")?)), + "negate" => Value::Color(color.negate()), + _ => return missing(), + }, + + Value::Str(string) => match method { + "len" => Value::Int(string.len() as i64), + "first" => Value::Str(string.first().at(span)?), + "last" => Value::Str(string.last().at(span)?), + "at" => Value::Str(string.at(args.expect("index")?).at(span)?), + "slice" => { + let start = args.expect("start")?; + let mut end = args.eat()?; + if end.is_none() { + end = args.named("count")?.map(|c: i64| start + c); + } + Value::Str(string.slice(start, end).at(span)?) + } + "clusters" => Value::Array(string.clusters()), + "codepoints" => Value::Array(string.codepoints()), + "contains" => Value::Bool(string.contains(args.expect("pattern")?)), + "starts-with" => Value::Bool(string.starts_with(args.expect("pattern")?)), + "ends-with" => Value::Bool(string.ends_with(args.expect("pattern")?)), + "find" => { + string.find(args.expect("pattern")?).map_or(Value::None, Value::Str) + } + "position" => string + .position(args.expect("pattern")?) + .map_or(Value::None, Value::Int), + "match" => string + .match_(args.expect("pattern")?) + .map_or(Value::None, Value::Dict), + "matches" => Value::Array(string.matches(args.expect("pattern")?)), + "replace" => { + let pattern = args.expect("pattern")?; + let with = args.expect("replacement string")?; + let count = args.named("count")?; + Value::Str(string.replace(pattern, with, count)) + } + "trim" => { + let pattern = args.eat()?; + let at = args.named("at")?; + let repeat = args.named("repeat")?.unwrap_or(true); + Value::Str(string.trim(pattern, at, repeat)) + } + "split" => Value::Array(string.split(args.eat()?)), + _ => return missing(), + }, + + Value::Array(array) => match method { + "len" => Value::Int(array.len()), + "first" => array.first().at(span)?.clone(), + "last" => array.last().at(span)?.clone(), + "at" => array.at(args.expect("index")?).at(span)?.clone(), + "slice" => { + let start = args.expect("start")?; + let mut end = args.eat()?; + if end.is_none() { + end = args.named("count")?.map(|c: i64| start + c); + } + Value::Array(array.slice(start, end).at(span)?) + } + "contains" => Value::Bool(array.contains(&args.expect("value")?)), + "find" => array.find(vm, args.expect("function")?)?.unwrap_or(Value::None), + "position" => array + .position(vm, args.expect("function")?)? + .map_or(Value::None, Value::Int), + "filter" => Value::Array(array.filter(vm, args.expect("function")?)?), + "map" => Value::Array(array.map(vm, args.expect("function")?)?), + "fold" => { + array.fold(vm, args.expect("initial value")?, args.expect("function")?)? + } + "any" => Value::Bool(array.any(vm, args.expect("function")?)?), + "all" => Value::Bool(array.all(vm, args.expect("function")?)?), + "flatten" => Value::Array(array.flatten()), + "rev" => Value::Array(array.rev()), + "join" => { + let sep = args.eat()?; + let last = args.named("last")?; + array.join(sep, last).at(span)? + } + "sorted" => Value::Array(array.sorted().at(span)?), + _ => return missing(), + }, + + Value::Dict(dict) => match method { + "len" => Value::Int(dict.len()), + "at" => dict.at(&args.expect::<Str>("key")?).cloned().at(span)?, + "keys" => Value::Array(dict.keys()), + "values" => Value::Array(dict.values()), + "pairs" => Value::Array(dict.pairs()), + _ => return missing(), + }, + + Value::Func(func) => match method { + "with" => Value::Func(func.with(args.take())), + "where" => Value::dynamic(func.where_(&mut args).at(span)?), + _ => return missing(), + }, + + Value::Args(args) => match method { + "pos" => Value::Array(args.to_pos()), + "named" => Value::Dict(args.to_named()), + _ => return missing(), + }, + + _ => return missing(), + }; + + args.finish()?; + Ok(output) +} + +/// Call a mutating method on a value. +pub fn call_mut( + value: &mut Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + let mut output = Value::None; + + match value { + Value::Array(array) => match method { + "push" => array.push(args.expect("value")?), + "pop" => output = array.pop().at(span)?, + "insert" => { + array.insert(args.expect("index")?, args.expect("value")?).at(span)? + } + "remove" => output = array.remove(args.expect("index")?).at(span)?, + _ => return missing(), + }, + + Value::Dict(dict) => match method { + "insert" => dict.insert(args.expect::<Str>("key")?, args.expect("value")?), + "remove" => { + output = dict.remove(&args.expect::<EcoString>("key")?).at(span)? + } + _ => return missing(), + }, + + _ => return missing(), + } + + args.finish()?; + Ok(output) +} + +/// Call an accessor method on a value. +pub fn call_access<'a>( + value: &'a mut Value, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult<&'a mut Value> { + let name = value.type_name(); + let missing = || Err(missing_method(name, method)).at(span); + + let slot = match value { + Value::Array(array) => match method { + "first" => array.first_mut().at(span)?, + "last" => array.last_mut().at(span)?, + "at" => array.at_mut(args.expect("index")?).at(span)?, + _ => return missing(), + }, + Value::Dict(dict) => match method { + "at" => dict.at_mut(&args.expect::<Str>("key")?).at(span)?, + _ => return missing(), + }, + _ => return missing(), + }; + + args.finish()?; + Ok(slot) +} + +/// Whether a specific method is mutating. +pub fn is_mutating(method: &str) -> bool { + matches!(method, "push" | "pop" | "insert" | "remove") +} + +/// Whether a specific method is an accessor. +pub fn is_accessor(method: &str) -> bool { + matches!(method, "first" | "last" | "at") +} + +/// The missing method error message. +#[cold] +fn missing_method(type_name: &str, method: &str) -> String { + format!("type {type_name} has no method `{method}`") +} + +/// List the available methods for a type and whether they take arguments. +pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { + match type_name { + "color" => &[("lighten", true), ("darken", true), ("negate", false)], + "string" => &[ + ("len", false), + ("at", true), + ("clusters", false), + ("codepoints", false), + ("contains", true), + ("ends-with", true), + ("find", true), + ("first", false), + ("last", false), + ("match", true), + ("matches", true), + ("position", true), + ("replace", true), + ("slice", true), + ("split", true), + ("starts-with", true), + ("trim", true), + ], + "array" => &[ + ("all", true), + ("any", true), + ("at", true), + ("contains", true), + ("filter", true), + ("find", true), + ("first", false), + ("flatten", false), + ("fold", true), + ("insert", true), + ("join", true), + ("last", false), + ("len", false), + ("map", true), + ("pop", false), + ("position", true), + ("push", true), + ("remove", true), + ("rev", false), + ("slice", true), + ("sorted", false), + ], + "dictionary" => &[ + ("at", true), + ("insert", true), + ("keys", false), + ("len", false), + ("pairs", false), + ("remove", true), + ("values", false), + ], + "function" => &[("where", true), ("with", true)], + "arguments" => &[("named", false), ("pos", false)], + _ => &[], + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs new file mode 100644 index 00000000..2cf6f4d1 --- /dev/null +++ b/src/eval/mod.rs @@ -0,0 +1,1554 @@ +//! Evaluation of markup into modules. + +#[macro_use] +mod library; +#[macro_use] +mod cast; +#[macro_use] +mod array; +#[macro_use] +mod dict; +#[macro_use] +mod str; +#[macro_use] +mod value; +mod args; +mod func; +mod methods; +mod module; +mod ops; +mod scope; +mod symbol; + +pub use typst_macros::{castable, func}; + +pub use self::args::*; +pub use self::array::*; +pub use self::cast::*; +pub use self::dict::*; +pub use self::func::*; +pub use self::library::*; +pub use self::methods::*; +pub use self::module::*; +pub use self::scope::*; +pub use self::str::*; +pub use self::symbol::*; +pub use self::value::*; + +use std::collections::BTreeMap; +use std::mem; +use std::path::{Path, PathBuf}; + +use comemo::{Track, Tracked, TrackedMut}; +use ecow::EcoVec; +use unicode_segmentation::UnicodeSegmentation; + +use crate::diag::{ + bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, +}; +use crate::model::{Content, Label, Recipe, Selector, StyleMap, Transform}; +use crate::syntax::ast::AstNode; +use crate::syntax::{ + ast, parse_code, Source, SourceId, Span, Spanned, SyntaxKind, SyntaxNode, +}; +use crate::util::PathExt; +use crate::World; + +const MAX_ITERATIONS: usize = 10_000; +const MAX_CALL_DEPTH: usize = 256; + +/// Evaluate a source file and return the resulting module. +#[comemo::memoize] +pub fn eval( + world: Tracked<dyn World>, + route: Tracked<Route>, + tracer: TrackedMut<Tracer>, + source: &Source, +) -> SourceResult<Module> { + // Prevent cyclic evaluation. + let id = source.id(); + let path = if id.is_detached() { Path::new("") } else { world.source(id).path() }; + if route.contains(id) { + panic!("Tried to cyclicly evaluate {}", path.display()); + } + + // Hook up the lang items. + let library = world.library(); + set_lang_items(library.items.clone()); + + // Evaluate the module. + let route = unsafe { Route::insert(route, id) }; + let scopes = Scopes::new(Some(library)); + let mut vm = Vm::new(world, route.track(), tracer, id, scopes, 0); + let root = match source.root().cast::<ast::Markup>() { + Some(markup) if vm.traced.is_some() => markup, + _ => source.ast()?, + }; + + let result = root.eval(&mut vm); + + // Handle control flow. + if let Some(flow) = vm.flow { + bail!(flow.forbidden()); + } + + // Assemble the module. + let name = path.file_stem().unwrap_or_default().to_string_lossy(); + Ok(Module::new(name).with_scope(vm.scopes.top).with_content(result?)) +} + +/// Evaluate a string as code and return the resulting value. +/// +/// Everything in the output is associated with the given `span`. +#[comemo::memoize] +pub fn eval_code_str( + world: Tracked<dyn World>, + text: &str, + span: Span, +) -> SourceResult<Value> { + let mut root = parse_code(text); + root.synthesize(span); + + let errors = root.errors(); + if !errors.is_empty() { + return Err(Box::new(errors)); + } + + let id = SourceId::detached(); + let library = world.library(); + let scopes = Scopes::new(Some(library)); + let route = Route::default(); + let mut tracer = Tracer::default(); + let mut vm = Vm::new(world, route.track(), tracer.track_mut(), id, scopes, 0); + let code = root.cast::<ast::Code>().unwrap(); + let result = code.eval(&mut vm); + + // Handle control flow. + if let Some(flow) = vm.flow { + bail!(flow.forbidden()); + } + + result +} + +/// A virtual machine. +/// +/// Holds the state needed to [evaluate](eval) Typst sources. A new +/// virtual machine is created for each module evaluation and function call. +pub struct Vm<'a> { + /// The compilation environment. + pub(super) world: Tracked<'a, dyn World>, + /// The language items. + pub(super) items: LangItems, + /// The route of source ids the VM took to reach its current location. + pub(super) route: Tracked<'a, Route>, + /// The tracer for inspection of the values an expression produces. + pub(super) tracer: TrackedMut<'a, Tracer>, + /// The current location. + pub(super) location: SourceId, + /// A control flow event that is currently happening. + pub(super) flow: Option<Flow>, + /// The stack of scopes. + pub(super) scopes: Scopes<'a>, + /// The current call depth. + pub(super) depth: usize, + /// A span that is currently traced. + pub(super) traced: Option<Span>, +} + +impl<'a> Vm<'a> { + /// Create a new virtual machine. + pub(super) fn new( + world: Tracked<'a, dyn World>, + route: Tracked<'a, Route>, + tracer: TrackedMut<'a, Tracer>, + location: SourceId, + scopes: Scopes<'a>, + depth: usize, + ) -> Self { + let traced = tracer.span(location); + Self { + world, + items: world.library().items.clone(), + route, + tracer, + location, + flow: None, + scopes, + depth, + traced, + } + } + + /// Access the underlying world. + pub fn world(&self) -> Tracked<'a, dyn World> { + self.world + } + + /// Define a variable in the current scope. + pub fn define(&mut self, var: ast::Ident, value: impl Into<Value>) { + let value = value.into(); + if self.traced == Some(var.span()) { + self.tracer.trace(value.clone()); + } + self.scopes.top.define(var.take(), value); + } + + /// Resolve a user-entered path to be relative to the compilation + /// environment's root. + pub fn locate(&self, path: &str) -> StrResult<PathBuf> { + if !self.location.is_detached() { + if let Some(path) = path.strip_prefix('/') { + return Ok(self.world.root().join(path).normalize()); + } + + if let Some(dir) = self.world.source(self.location).path().parent() { + return Ok(dir.join(path).normalize()); + } + } + + Err("cannot access file system from here".into()) + } +} + +/// A control flow event that occurred during evaluation. +#[derive(Debug, Clone, PartialEq)] +pub enum Flow { + /// Stop iteration in a loop. + Break(Span), + /// Skip the remainder of the current iteration in a loop. + Continue(Span), + /// Stop execution of a function early, optionally returning an explicit + /// value. + Return(Span, Option<Value>), +} + +impl Flow { + /// Return an error stating that this control flow is forbidden. + pub fn forbidden(&self) -> SourceError { + match *self { + Self::Break(span) => { + error!(span, "cannot break outside of loop") + } + Self::Continue(span) => { + error!(span, "cannot continue outside of loop") + } + Self::Return(span, _) => { + error!(span, "cannot return outside of function") + } + } + } +} + +/// A route of source ids. +#[derive(Default)] +pub struct Route { + parent: Option<Tracked<'static, Self>>, + id: Option<SourceId>, +} + +impl Route { + /// Create a new route with just one entry. + pub fn new(id: SourceId) -> Self { + Self { id: Some(id), parent: None } + } + + /// Insert a new id into the route. + /// + /// You must guarantee that `outer` lives longer than the resulting + /// route is ever used. + unsafe fn insert(outer: Tracked<Route>, id: SourceId) -> Route { + Route { + parent: Some(std::mem::transmute(outer)), + id: Some(id), + } + } +} + +#[comemo::track] +impl Route { + /// Whether the given id is part of the route. + fn contains(&self, id: SourceId) -> bool { + self.id == Some(id) || self.parent.map_or(false, |parent| parent.contains(id)) + } +} + +/// Traces which values existed for the expression with the given span. +#[derive(Default, Clone)] +pub struct Tracer { + span: Option<Span>, + values: Vec<Value>, +} + +impl Tracer { + /// The maximum number of traced items. + pub const MAX: usize = 10; + + /// Create a new tracer, possibly with a span under inspection. + pub fn new(span: Option<Span>) -> Self { + Self { span, values: vec![] } + } + + /// Get the traced values. + pub fn finish(self) -> Vec<Value> { + self.values + } +} + +#[comemo::track] +impl Tracer { + /// The traced span if it is part of the given source file. + fn span(&self, id: SourceId) -> Option<Span> { + if self.span.map(Span::source) == Some(id) { + self.span + } else { + None + } + } + + /// Trace a value for the span. + fn trace(&mut self, v: Value) { + if self.values.len() < Self::MAX { + self.values.push(v); + } + } +} + +/// Evaluate an expression. +pub(super) trait Eval { + /// The output of evaluating the expression. + type Output; + + /// Evaluate the expression to the output value. + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output>; +} + +impl Eval for ast::Markup { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + eval_markup(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of markup. +fn eval_markup( + vm: &mut Vm, + exprs: &mut impl Iterator<Item = ast::Expr>, +) -> SourceResult<Content> { + let flow = vm.flow.take(); + let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default()); + + while let Some(expr) = exprs.next() { + match expr { + ast::Expr::Set(set) => { + let styles = set.eval(vm)?; + if vm.flow.is_some() { + break; + } + + seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) + } + ast::Expr::Show(show) => { + let recipe = show.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_markup(vm, exprs)?; + seq.push(tail.styled_with_recipe(vm.world, recipe)?) + } + expr => match expr.eval(vm)? { + Value::Label(label) => { + if let Some(node) = + seq.iter_mut().rev().find(|node| node.labellable()) + { + *node = mem::take(node).labelled(label); + } + } + value => seq.push(value.display()), + }, + } + + if vm.flow.is_some() { + break; + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(Content::sequence(seq)) +} + +impl Eval for ast::Expr { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.span(); + let forbidden = |name| { + error!(span, "{} is only allowed directly in code and content blocks", name) + }; + + let v = match self { + Self::Text(v) => v.eval(vm).map(Value::Content), + Self::Space(v) => v.eval(vm).map(Value::Content), + Self::Linebreak(v) => v.eval(vm).map(Value::Content), + Self::Parbreak(v) => v.eval(vm).map(Value::Content), + Self::Escape(v) => v.eval(vm), + Self::Shorthand(v) => v.eval(vm), + Self::SmartQuote(v) => v.eval(vm).map(Value::Content), + Self::Strong(v) => v.eval(vm).map(Value::Content), + Self::Emph(v) => v.eval(vm).map(Value::Content), + Self::Raw(v) => v.eval(vm).map(Value::Content), + Self::Link(v) => v.eval(vm).map(Value::Content), + Self::Label(v) => v.eval(vm), + Self::Ref(v) => v.eval(vm).map(Value::Content), + Self::Heading(v) => v.eval(vm).map(Value::Content), + Self::List(v) => v.eval(vm).map(Value::Content), + Self::Enum(v) => v.eval(vm).map(Value::Content), + Self::Term(v) => v.eval(vm).map(Value::Content), + Self::Formula(v) => v.eval(vm).map(Value::Content), + Self::Math(v) => v.eval(vm).map(Value::Content), + Self::MathIdent(v) => v.eval(vm), + Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), + Self::MathDelimited(v) => v.eval(vm).map(Value::Content), + Self::MathAttach(v) => v.eval(vm).map(Value::Content), + Self::MathFrac(v) => v.eval(vm).map(Value::Content), + Self::Ident(v) => v.eval(vm), + Self::None(v) => v.eval(vm), + Self::Auto(v) => v.eval(vm), + Self::Bool(v) => v.eval(vm), + Self::Int(v) => v.eval(vm), + Self::Float(v) => v.eval(vm), + Self::Numeric(v) => v.eval(vm), + Self::Str(v) => v.eval(vm), + Self::Code(v) => v.eval(vm), + Self::Content(v) => v.eval(vm).map(Value::Content), + Self::Array(v) => v.eval(vm).map(Value::Array), + Self::Dict(v) => v.eval(vm).map(Value::Dict), + Self::Parenthesized(v) => v.eval(vm), + Self::FieldAccess(v) => v.eval(vm), + Self::FuncCall(v) => v.eval(vm), + Self::Closure(v) => v.eval(vm), + Self::Unary(v) => v.eval(vm), + Self::Binary(v) => v.eval(vm), + Self::Let(v) => v.eval(vm), + Self::Set(_) => bail!(forbidden("set")), + Self::Show(_) => bail!(forbidden("show")), + Self::Conditional(v) => v.eval(vm), + Self::While(v) => v.eval(vm), + Self::For(v) => v.eval(vm), + Self::Import(v) => v.eval(vm), + Self::Include(v) => v.eval(vm).map(Value::Content), + Self::Break(v) => v.eval(vm), + Self::Continue(v) => v.eval(vm), + Self::Return(v) => v.eval(vm), + }? + .spanned(span); + + if vm.traced == Some(span) { + vm.tracer.trace(v.clone()); + } + + Ok(v) + } +} + +impl Eval for ast::Text { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.text)(self.get().clone())) + } +} + +impl Eval for ast::Space { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.space)()) + } +} + +impl Eval for ast::Linebreak { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.linebreak)()) + } +} + +impl Eval for ast::Parbreak { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.parbreak)()) + } +} + +impl Eval for ast::Escape { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Symbol(Symbol::new(self.get()))) + } +} + +impl Eval for ast::Shorthand { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Symbol(Symbol::new(self.get()))) + } +} + +impl Eval for ast::SmartQuote { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.smart_quote)(self.double())) + } +} + +impl Eval for ast::Strong { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.strong)(self.body().eval(vm)?)) + } +} + +impl Eval for ast::Emph { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.emph)(self.body().eval(vm)?)) + } +} + +impl Eval for ast::Raw { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let text = self.text(); + let lang = self.lang().map(Into::into); + let block = self.block(); + Ok((vm.items.raw)(text, lang, block)) + } +} + +impl Eval for ast::Link { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.link)(self.get().clone())) + } +} + +impl Eval for ast::Label { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Label(Label(self.get().into()))) + } +} + +impl Eval for ast::Ref { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.ref_)(self.get().into())) + } +} + +impl Eval for ast::Heading { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let level = self.level(); + let body = self.body().eval(vm)?; + Ok((vm.items.heading)(level, body)) + } +} + +impl Eval for ast::ListItem { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.list_item)(self.body().eval(vm)?)) + } +} + +impl Eval for ast::EnumItem { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let number = self.number(); + let body = self.body().eval(vm)?; + Ok((vm.items.enum_item)(number, body)) + } +} + +impl Eval for ast::TermItem { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let term = self.term().eval(vm)?; + let description = self.description().eval(vm)?; + Ok((vm.items.term_item)(term, description)) + } +} + +impl Eval for ast::Formula { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body().eval(vm)?; + let block = self.block(); + Ok((vm.items.formula)(body, block)) + } +} + +impl Eval for ast::Math { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok(Content::sequence( + self.exprs() + .map(|expr| Ok(expr.eval(vm)?.display())) + .collect::<SourceResult<_>>()?, + ) + .spanned(self.span())) + } +} + +impl Eval for ast::MathIdent { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok(vm.scopes.get_in_math(self).cloned().at(self.span())?) + } +} + +impl Eval for ast::MathAlignPoint { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok((vm.items.math_align_point)()) + } +} + +impl Eval for ast::MathDelimited { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let open = self.open().eval(vm)?.display(); + let body = self.body().eval(vm)?; + let close = self.close().eval(vm)?.display(); + Ok((vm.items.math_delimited)(open, body, close)) + } +} + +impl Eval for ast::MathAttach { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let base = self.base().eval(vm)?.display(); + let bottom = self + .bottom() + .map(|expr| expr.eval(vm).map(Value::display)) + .transpose()?; + let top = self.top().map(|expr| expr.eval(vm).map(Value::display)).transpose()?; + Ok((vm.items.math_attach)(base, bottom, top)) + } +} + +impl Eval for ast::MathFrac { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let num = self.num().eval(vm)?.display(); + let denom = self.denom().eval(vm)?.display(); + Ok((vm.items.math_frac)(num, denom)) + } +} + +impl Eval for ast::Ident { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok(vm.scopes.get(self).cloned().at(self.span())?) + } +} + +impl Eval for ast::None { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::None) + } +} + +impl Eval for ast::Auto { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Auto) + } +} + +impl Eval for ast::Bool { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Bool(self.get())) + } +} + +impl Eval for ast::Int { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Int(self.get())) + } +} + +impl Eval for ast::Float { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Float(self.get())) + } +} + +impl Eval for ast::Numeric { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::numeric(self.get())) + } +} + +impl Eval for ast::Str { + type Output = Value; + + fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Str(self.get().into())) + } +} + +impl Eval for ast::CodeBlock { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.enter(); + let output = self.body().eval(vm)?; + vm.scopes.exit(); + Ok(output) + } +} + +impl Eval for ast::Code { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + eval_code(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of expressions. +fn eval_code( + vm: &mut Vm, + exprs: &mut impl Iterator<Item = ast::Expr>, +) -> SourceResult<Value> { + let flow = vm.flow.take(); + let mut output = Value::None; + + while let Some(expr) = exprs.next() { + let span = expr.span(); + let value = match expr { + ast::Expr::Set(set) => { + let styles = set.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_code(vm, exprs)?.display(); + Value::Content(tail.styled_with_map(styles)) + } + ast::Expr::Show(show) => { + let recipe = show.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_code(vm, exprs)?.display(); + Value::Content(tail.styled_with_recipe(vm.world, recipe)?) + } + _ => expr.eval(vm)?, + }; + + output = ops::join(output, value).at(span)?; + + if vm.flow.is_some() { + break; + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) +} + +impl Eval for ast::ContentBlock { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.enter(); + let content = self.body().eval(vm)?; + vm.scopes.exit(); + Ok(content) + } +} + +impl Eval for ast::Parenthesized { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + self.expr().eval(vm) + } +} + +impl Eval for ast::Array { + type Output = Array; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let items = self.items(); + + let mut vec = EcoVec::with_capacity(items.size_hint().0); + for item in items { + match item { + ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?), + ast::ArrayItem::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Array(array) => vec.extend(array.into_iter()), + v => bail!(expr.span(), "cannot spread {} into array", v.type_name()), + }, + } + } + + Ok(Array::from_vec(vec)) + } +} + +impl Eval for ast::Dict { + type Output = Dict; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let mut map = BTreeMap::new(); + + for item in self.items() { + match item { + ast::DictItem::Named(named) => { + map.insert(named.name().take().into(), named.expr().eval(vm)?); + } + ast::DictItem::Keyed(keyed) => { + map.insert(keyed.key().get().into(), keyed.expr().eval(vm)?); + } + ast::DictItem::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Dict(dict) => map.extend(dict.into_iter()), + v => bail!( + expr.span(), + "cannot spread {} into dictionary", + v.type_name() + ), + }, + } + } + + Ok(Dict::from_map(map)) + } +} + +impl Eval for ast::Unary { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.expr().eval(vm)?; + let result = match self.op() { + ast::UnOp::Pos => ops::pos(value), + ast::UnOp::Neg => ops::neg(value), + ast::UnOp::Not => ops::not(value), + }; + result.at(self.span()) + } +} + +impl Eval for ast::Binary { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + match self.op() { + ast::BinOp::Add => self.apply(vm, ops::add), + ast::BinOp::Sub => self.apply(vm, ops::sub), + ast::BinOp::Mul => self.apply(vm, ops::mul), + ast::BinOp::Div => self.apply(vm, ops::div), + ast::BinOp::And => self.apply(vm, ops::and), + ast::BinOp::Or => self.apply(vm, ops::or), + ast::BinOp::Eq => self.apply(vm, ops::eq), + ast::BinOp::Neq => self.apply(vm, ops::neq), + ast::BinOp::Lt => self.apply(vm, ops::lt), + ast::BinOp::Leq => self.apply(vm, ops::leq), + ast::BinOp::Gt => self.apply(vm, ops::gt), + ast::BinOp::Geq => self.apply(vm, ops::geq), + ast::BinOp::In => self.apply(vm, ops::in_), + ast::BinOp::NotIn => self.apply(vm, ops::not_in), + ast::BinOp::Assign => self.assign(vm, |_, b| Ok(b)), + ast::BinOp::AddAssign => self.assign(vm, ops::add), + ast::BinOp::SubAssign => self.assign(vm, ops::sub), + ast::BinOp::MulAssign => self.assign(vm, ops::mul), + ast::BinOp::DivAssign => self.assign(vm, ops::div), + } + } +} + +impl ast::Binary { + /// Apply a basic binary operation. + fn apply( + &self, + vm: &mut Vm, + op: fn(Value, Value) -> StrResult<Value>, + ) -> SourceResult<Value> { + let lhs = self.lhs().eval(vm)?; + + // Short-circuit boolean operations. + if (self.op() == ast::BinOp::And && lhs == Value::Bool(false)) + || (self.op() == ast::BinOp::Or && lhs == Value::Bool(true)) + { + return Ok(lhs); + } + + let rhs = self.rhs().eval(vm)?; + op(lhs, rhs).at(self.span()) + } + + /// Apply an assignment operation. + fn assign( + &self, + vm: &mut Vm, + op: fn(Value, Value) -> StrResult<Value>, + ) -> SourceResult<Value> { + let rhs = self.rhs().eval(vm)?; + let lhs = self.lhs(); + + // An assignment to a dictionary field is different from a normal access + // since it can create the field instead of just modifying it. + if self.op() == ast::BinOp::Assign { + if let ast::Expr::FieldAccess(access) = &lhs { + let dict = access.access_dict(vm)?; + dict.insert(access.field().take().into(), rhs); + return Ok(Value::None); + } + } + + let location = self.lhs().access(vm)?; + let lhs = std::mem::take(&mut *location); + *location = op(lhs, rhs).at(self.span())?; + Ok(Value::None) + } +} + +impl Eval for ast::FieldAccess { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.target().eval(vm)?; + let field = self.field(); + value.field(&field).at(field.span()) + } +} + +impl Eval for ast::FuncCall { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.span(); + let callee = self.callee(); + let in_math = in_math(&callee); + let callee_span = callee.span(); + let args = self.args(); + + // Try to evaluate as a method call. This is possible if the callee is a + // field access and does not evaluate to a module. + let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { + let target = access.target(); + let field = access.field(); + let field_span = field.span(); + let field = field.take(); + let point = || Tracepoint::Call(Some(field.clone())); + if methods::is_mutating(&field) { + let args = args.eval(vm)?; + let target = target.access(vm)?; + if !matches!(target, Value::Symbol(_) | Value::Module(_)) { + return methods::call_mut(target, &field, args, span) + .trace(vm.world, point, span); + } + (target.field(&field).at(field_span)?, args) + } else { + let target = target.eval(vm)?; + let args = args.eval(vm)?; + if !matches!(target, Value::Symbol(_) | Value::Module(_)) { + return methods::call(vm, target, &field, args, span) + .trace(vm.world, point, span); + } + (target.field(&field).at(field_span)?, args) + } + } else { + (callee.eval(vm)?, args.eval(vm)?) + }; + + // Handle math special cases for non-functions: + // Combining accent symbols apply themselves while everything else + // simply displays the arguments verbatim. + if in_math && !matches!(callee, Value::Func(_)) { + if let Value::Symbol(sym) = &callee { + let c = sym.get(); + if let Some(accent) = combining_accent(c) { + let base = args.expect("base")?; + args.finish()?; + return Ok(Value::Content((vm.items.math_accent)(base, accent))); + } + } + let mut body = Content::empty(); + for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { + if i > 0 { + body += (vm.items.text)(','.into()); + } + body += arg; + } + return Ok(Value::Content( + callee.display() + + (vm.items.math_delimited)( + (vm.items.text)('('.into()), + body, + (vm.items.text)(')'.into()), + ), + )); + } + + // Finally, just a normal function call! + if vm.depth >= MAX_CALL_DEPTH { + bail!(span, "maximum function call depth exceeded"); + } + + let callee = callee.cast::<Func>().at(callee_span)?; + let point = || Tracepoint::Call(callee.name().map(Into::into)); + callee.call(vm, args).trace(vm.world, point, span) + } +} + +fn in_math(expr: &ast::Expr) -> bool { + match expr { + ast::Expr::MathIdent(_) => true, + ast::Expr::FieldAccess(access) => in_math(&access.target()), + _ => false, + } +} + +impl Eval for ast::Args { + type Output = Args; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let mut items = EcoVec::new(); + + for arg in self.items() { + let span = arg.span(); + match arg { + ast::Arg::Pos(expr) => { + items.push(Arg { + span, + name: None, + value: Spanned::new(expr.eval(vm)?, expr.span()), + }); + } + ast::Arg::Named(named) => { + items.push(Arg { + span, + name: Some(named.name().take().into()), + value: Spanned::new(named.expr().eval(vm)?, named.expr().span()), + }); + } + ast::Arg::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Array(array) => { + items.extend(array.into_iter().map(|value| Arg { + span, + name: None, + value: Spanned::new(value, span), + })); + } + Value::Dict(dict) => { + items.extend(dict.into_iter().map(|(key, value)| Arg { + span, + name: Some(key), + value: Spanned::new(value, span), + })); + } + Value::Args(args) => items.extend(args.items), + v => bail!(expr.span(), "cannot spread {}", v.type_name()), + }, + } + } + + Ok(Args { span: self.span(), items }) + } +} + +impl Eval for ast::Closure { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + // The closure's name is defined by its let binding if there's one. + let name = self.name().map(ast::Ident::take); + + // Collect captured variables. + let captured = { + let mut visitor = CapturesVisitor::new(&vm.scopes); + visitor.visit(self.as_untyped()); + visitor.finish() + }; + + let mut params = Vec::new(); + let mut sink = None; + + // Collect parameters and an optional sink parameter. + for param in self.params() { + match param { + ast::Param::Pos(name) => { + params.push((name.take(), None)); + } + ast::Param::Named(named) => { + params.push((named.name().take(), Some(named.expr().eval(vm)?))); + } + ast::Param::Sink(name) => { + if sink.is_some() { + bail!(name.span(), "only one argument sink is allowed"); + } + sink = Some(name.take()); + } + } + } + + // Define the closure. + let closure = Closure { + location: vm.location, + name, + captured, + params, + sink, + body: self.body(), + }; + + Ok(Value::Func(Func::from_closure(closure, self.span()))) + } +} + +impl Eval for ast::LetBinding { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = match self.init() { + Some(expr) => expr.eval(vm)?, + None => Value::None, + }; + vm.define(self.binding(), value); + Ok(Value::None) + } +} + +impl Eval for ast::SetRule { + type Output = StyleMap; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if let Some(condition) = self.condition() { + if !condition.eval(vm)?.cast::<bool>().at(condition.span())? { + return Ok(StyleMap::new()); + } + } + + let target = self.target(); + let target = target.eval(vm)?.cast::<Func>().at(target.span())?; + let args = self.args().eval(vm)?; + Ok(target.set(args)?.spanned(self.span())) + } +} + +impl Eval for ast::ShowRule { + type Output = Recipe; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let selector = self + .selector() + .map(|sel| sel.eval(vm)?.cast::<Selector>().at(sel.span())) + .transpose()?; + + let transform = self.transform(); + let span = transform.span(); + + let transform = match transform { + ast::Expr::Set(set) => Transform::Style(set.eval(vm)?), + expr => expr.eval(vm)?.cast::<Transform>().at(span)?, + }; + + Ok(Recipe { span, selector, transform }) + } +} + +impl Eval for ast::Conditional { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let condition = self.condition(); + if condition.eval(vm)?.cast::<bool>().at(condition.span())? { + self.if_body().eval(vm) + } else if let Some(else_body) = self.else_body() { + else_body.eval(vm) + } else { + Ok(Value::None) + } + } +} + +impl Eval for ast::WhileLoop { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let flow = vm.flow.take(); + let mut output = Value::None; + let mut i = 0; + + let condition = self.condition(); + let body = self.body(); + + while condition.eval(vm)?.cast::<bool>().at(condition.span())? { + if i == 0 + && is_invariant(condition.as_untyped()) + && !can_diverge(body.as_untyped()) + { + bail!(condition.span(), "condition is always true"); + } else if i >= MAX_ITERATIONS { + bail!(self.span(), "loop seems to be infinite"); + } + + let value = body.eval(vm)?; + output = ops::join(output, value).at(body.span())?; + + match vm.flow { + Some(Flow::Break(_)) => { + vm.flow = None; + break; + } + Some(Flow::Continue(_)) => vm.flow = None, + Some(Flow::Return(..)) => break, + None => {} + } + + i += 1; + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) + } +} + +/// Whether the expression always evaluates to the same value. +fn is_invariant(expr: &SyntaxNode) -> bool { + match expr.cast() { + Some(ast::Expr::Ident(_)) => false, + Some(ast::Expr::MathIdent(_)) => false, + Some(ast::Expr::FieldAccess(access)) => { + is_invariant(access.target().as_untyped()) + } + Some(ast::Expr::FuncCall(call)) => { + is_invariant(call.callee().as_untyped()) + && is_invariant(call.args().as_untyped()) + } + _ => expr.children().all(is_invariant), + } +} + +/// Whether the expression contains a break or return. +fn can_diverge(expr: &SyntaxNode) -> bool { + matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return) + || expr.children().any(can_diverge) +} + +impl Eval for ast::ForLoop { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let flow = vm.flow.take(); + let mut output = Value::None; + + macro_rules! iter { + (for ($($binding:ident => $value:ident),*) in $iter:expr) => {{ + vm.scopes.enter(); + + #[allow(unused_parens)] + for ($($value),*) in $iter { + $(vm.define($binding.clone(), $value);)* + + let body = self.body(); + let value = body.eval(vm)?; + output = ops::join(output, value).at(body.span())?; + + match vm.flow { + Some(Flow::Break(_)) => { + vm.flow = None; + break; + } + Some(Flow::Continue(_)) => vm.flow = None, + Some(Flow::Return(..)) => break, + None => {} + } + } + + vm.scopes.exit(); + }}; + } + + let iter = self.iter().eval(vm)?; + let pattern = self.pattern(); + let key = pattern.key(); + let value = pattern.value(); + + match (key, value, iter) { + (None, v, Value::Str(string)) => { + iter!(for (v => value) in string.as_str().graphemes(true)); + } + (None, v, Value::Array(array)) => { + iter!(for (v => value) in array.into_iter()); + } + (Some(i), v, Value::Array(array)) => { + iter!(for (i => idx, v => value) in array.into_iter().enumerate()); + } + (None, v, Value::Dict(dict)) => { + iter!(for (v => value) in dict.into_iter().map(|p| p.1)); + } + (Some(k), v, Value::Dict(dict)) => { + iter!(for (k => key, v => value) in dict.into_iter()); + } + (None, v, Value::Args(args)) => { + iter!(for (v => value) in args.items.into_iter() + .filter(|arg| arg.name.is_none()) + .map(|arg| arg.value.v)); + } + (Some(k), v, Value::Args(args)) => { + iter!(for (k => key, v => value) in args.items.into_iter() + .map(|arg| (arg.name.map_or(Value::None, Value::Str), arg.value.v))); + } + (_, _, Value::Str(_)) => { + bail!(pattern.span(), "mismatched pattern"); + } + (_, _, iter) => { + bail!(self.iter().span(), "cannot loop over {}", iter.type_name()); + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) + } +} + +impl Eval for ast::ModuleImport { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.source().span(); + let source = self.source().eval(vm)?; + let module = import(vm, source, span)?; + + match self.imports() { + None => { + vm.scopes.top.define(module.name().clone(), module); + } + Some(ast::Imports::Wildcard) => { + for (var, value) in module.scope().iter() { + vm.scopes.top.define(var.clone(), value.clone()); + } + } + Some(ast::Imports::Items(idents)) => { + let mut errors = vec![]; + for ident in idents { + if let Some(value) = module.scope().get(&ident) { + vm.define(ident, value.clone()); + } else { + errors.push(error!(ident.span(), "unresolved import")); + } + } + if !errors.is_empty() { + return Err(Box::new(errors)); + } + } + } + + Ok(Value::None) + } +} + +impl Eval for ast::ModuleInclude { + type Output = Content; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.source().span(); + let source = self.source().eval(vm)?; + let module = import(vm, source, span)?; + Ok(module.content()) + } +} + +/// Process an import of a module relative to the current location. +fn import(vm: &mut Vm, source: Value, span: Span) -> SourceResult<Module> { + let path = match source { + Value::Str(path) => path, + Value::Module(module) => return Ok(module), + v => bail!(span, "expected path or module, found {}", v.type_name()), + }; + + // Load the source file. + let full = vm.locate(&path).at(span)?; + let id = vm.world.resolve(&full).at(span)?; + + // Prevent cyclic importing. + if vm.route.contains(id) { + bail!(span, "cyclic import"); + } + + // Evaluate the file. + let source = vm.world.source(id); + let point = || Tracepoint::Import; + eval(vm.world, vm.route, TrackedMut::reborrow_mut(&mut vm.tracer), source) + .trace(vm.world, point, span) +} + +impl Eval for ast::LoopBreak { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if vm.flow.is_none() { + vm.flow = Some(Flow::Break(self.span())); + } + Ok(Value::None) + } +} + +impl Eval for ast::LoopContinue { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if vm.flow.is_none() { + vm.flow = Some(Flow::Continue(self.span())); + } + Ok(Value::None) + } +} + +impl Eval for ast::FuncReturn { + type Output = Value; + + fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.body().map(|body| body.eval(vm)).transpose()?; + if vm.flow.is_none() { + vm.flow = Some(Flow::Return(self.span(), value)); + } + Ok(Value::None) + } +} + +/// Access an expression mutably. +trait Access { + /// Access the value. + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>; +} + +impl Access for ast::Expr { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + match self { + Self::Ident(v) => v.access(vm), + Self::Parenthesized(v) => v.access(vm), + Self::FieldAccess(v) => v.access(vm), + Self::FuncCall(v) => v.access(vm), + _ => { + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } + } + } +} + +impl Access for ast::Ident { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + let span = self.span(); + let value = vm.scopes.get_mut(self).at(span)?; + if vm.traced == Some(span) { + vm.tracer.trace(value.clone()); + } + Ok(value) + } +} + +impl Access for ast::Parenthesized { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + self.expr().access(vm) + } +} + +impl Access for ast::FieldAccess { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + self.access_dict(vm)?.at_mut(&self.field().take()).at(self.span()) + } +} + +impl ast::FieldAccess { + fn access_dict<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Dict> { + match self.target().access(vm)? { + Value::Dict(dict) => Ok(dict), + value => bail!( + self.target().span(), + "expected dictionary, found {}", + value.type_name(), + ), + } + } +} + +impl Access for ast::FuncCall { + fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + if let ast::Expr::FieldAccess(access) = self.callee() { + let method = access.field().take(); + if methods::is_accessor(&method) { + let span = self.span(); + let world = vm.world(); + let args = self.args().eval(vm)?; + let value = access.target().access(vm)?; + let result = methods::call_access(value, &method, args, span); + let point = || Tracepoint::Call(Some(method.clone())); + return result.trace(world, point, span); + } + } + + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } +} diff --git a/src/eval/module.rs b/src/eval/module.rs new file mode 100644 index 00000000..e911d859 --- /dev/null +++ b/src/eval/module.rs @@ -0,0 +1,87 @@ +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; + +use super::{Content, Scope, Value}; +use crate::diag::StrResult; + +/// An evaluated module, ready for importing or typesetting. +#[derive(Clone, Hash)] +pub struct Module(Arc<Repr>); + +/// The internal representation. +#[derive(Clone, Hash)] +struct Repr { + /// The module's name. + name: EcoString, + /// The top-level definitions that were bound in this module. + scope: Scope, + /// The module's layoutable contents. + content: Content, +} + +impl Module { + /// Create a new module. + pub fn new(name: impl Into<EcoString>) -> Self { + Self(Arc::new(Repr { + name: name.into(), + scope: Scope::new(), + content: Content::empty(), + })) + } + + /// Update the module's scope. + pub fn with_scope(mut self, scope: Scope) -> Self { + Arc::make_mut(&mut self.0).scope = scope; + self + } + + /// Update the module's content. + pub fn with_content(mut self, content: Content) -> Self { + Arc::make_mut(&mut self.0).content = content; + self + } + + /// Get the module's name. + pub fn name(&self) -> &EcoString { + &self.0.name + } + + /// Access the module's scope. + pub fn scope(&self) -> &Scope { + &self.0.scope + } + + /// Access the module's scope, mutably. + pub fn scope_mut(&mut self) -> &mut Scope { + &mut Arc::make_mut(&mut self.0).scope + } + + /// Try to access a definition in the module. + pub fn get(&self, name: &str) -> StrResult<&Value> { + self.scope().get(&name).ok_or_else(|| { + eco_format!("module `{}` does not contain `{name}`", self.name()) + }) + } + + /// Extract the module's content. + pub fn content(self) -> Content { + match Arc::try_unwrap(self.0) { + Ok(repr) => repr.content, + Err(arc) => arc.content.clone(), + } + } +} + +impl Debug for Module { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "<module {}>", self.name()) + } +} + +impl PartialEq for Module { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} diff --git a/src/eval/ops.rs b/src/eval/ops.rs new file mode 100644 index 00000000..52b9b06a --- /dev/null +++ b/src/eval/ops.rs @@ -0,0 +1,414 @@ +//! Operations on values. + +use std::cmp::Ordering; + +use ecow::eco_format; + +use super::{format_str, Regex, Value}; +use crate::diag::StrResult; +use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; +use Value::*; + +/// Bail with a type mismatch error. +macro_rules! mismatch { + ($fmt:expr, $($value:expr),* $(,)?) => { + return Err(eco_format!($fmt, $($value.type_name()),*)) + }; +} + +/// Join a value with another value. +pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Str(a), Str(b)) => Str(a + b), + (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), + (Content(a), Str(b)) => Content(a + item!(text)(b.into())), + (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), + (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + (a, b) => mismatch!("cannot join {} with {}", a, b), + }) +} + +/// Apply the unary plus operator to a value. +pub fn pos(value: Value) -> StrResult<Value> { + Ok(match value { + Int(v) => Int(v), + Float(v) => Float(v), + Length(v) => Length(v), + Angle(v) => Angle(v), + Ratio(v) => Ratio(v), + Relative(v) => Relative(v), + Fraction(v) => Fraction(v), + v => mismatch!("cannot apply '+' to {}", v), + }) +} + +/// Compute the negation of a value. +pub fn neg(value: Value) -> StrResult<Value> { + Ok(match value { + Int(v) => Int(-v), + Float(v) => Float(-v), + Length(v) => Length(-v), + Angle(v) => Angle(-v), + Ratio(v) => Ratio(-v), + Relative(v) => Relative(-v), + Fraction(v) => Fraction(-v), + v => mismatch!("cannot apply '-' to {}", v), + }) +} + +/// Compute the sum of two values. +pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (a, None) => a, + (None, b) => b, + + (Int(a), Int(b)) => Int(a + b), + (Int(a), Float(b)) => Float(a as f64 + b), + (Float(a), Int(b)) => Float(a + b as f64), + (Float(a), Float(b)) => Float(a + b), + + (Angle(a), Angle(b)) => Angle(a + b), + + (Length(a), Length(b)) => Length(a + b), + (Length(a), Ratio(b)) => Relative(b + a), + (Length(a), Relative(b)) => Relative(b + a), + + (Ratio(a), Length(b)) => Relative(a + b), + (Ratio(a), Ratio(b)) => Ratio(a + b), + (Ratio(a), Relative(b)) => Relative(b + a), + + (Relative(a), Length(b)) => Relative(a + b), + (Relative(a), Ratio(b)) => Relative(a + b), + (Relative(a), Relative(b)) => Relative(a + b), + + (Fraction(a), Fraction(b)) => Fraction(a + b), + + (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Str(a), Str(b)) => Str(a + b), + (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), + (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), + (Content(a), Content(b)) => Content(a + b), + (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), + (Content(a), Str(b)) => Content(a + item!(text)(b.into())), + (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), + (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), + + (Array(a), Array(b)) => Array(a + b), + (Dict(a), Dict(b)) => Dict(a + b), + + (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { + Value::dynamic(PartialStroke { + paint: Smart::Custom(color.into()), + thickness: Smart::Custom(thickness), + }) + } + + (Dyn(a), Dyn(b)) => { + // 1D alignments can be summed into 2D alignments. + if let (Some(&a), Some(&b)) = + (a.downcast::<GenAlign>(), b.downcast::<GenAlign>()) + { + if a.axis() == b.axis() { + return Err(eco_format!("cannot add two {:?} alignments", a.axis())); + } + + return Ok(Value::dynamic(match a.axis() { + Axis::X => Axes { x: a, y: b }, + Axis::Y => Axes { x: b, y: a }, + })); + }; + + mismatch!("cannot add {} and {}", a, b); + } + + (a, b) => mismatch!("cannot add {} and {}", a, b), + }) +} + +/// Compute the difference of two values. +pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a - b), + (Int(a), Float(b)) => Float(a as f64 - b), + (Float(a), Int(b)) => Float(a - b as f64), + (Float(a), Float(b)) => Float(a - b), + + (Angle(a), Angle(b)) => Angle(a - b), + + (Length(a), Length(b)) => Length(a - b), + (Length(a), Ratio(b)) => Relative(-b + a), + (Length(a), Relative(b)) => Relative(-b + a), + + (Ratio(a), Length(b)) => Relative(a + -b), + (Ratio(a), Ratio(b)) => Ratio(a - b), + (Ratio(a), Relative(b)) => Relative(-b + a), + + (Relative(a), Length(b)) => Relative(a + -b), + (Relative(a), Ratio(b)) => Relative(a + -b), + (Relative(a), Relative(b)) => Relative(a - b), + + (Fraction(a), Fraction(b)) => Fraction(a - b), + + (a, b) => mismatch!("cannot subtract {1} from {0}", a, b), + }) +} + +/// Compute the product of two values. +pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Int(a * b), + (Int(a), Float(b)) => Float(a as f64 * b), + (Float(a), Int(b)) => Float(a * b as f64), + (Float(a), Float(b)) => Float(a * b), + + (Length(a), Int(b)) => Length(a * b as f64), + (Length(a), Float(b)) => Length(a * b), + (Int(a), Length(b)) => Length(b * a as f64), + (Float(a), Length(b)) => Length(b * a), + + (Angle(a), Int(b)) => Angle(a * b as f64), + (Angle(a), Float(b)) => Angle(a * b), + (Int(a), Angle(b)) => Angle(a as f64 * b), + (Float(a), Angle(b)) => Angle(a * b), + + (Ratio(a), Int(b)) => Ratio(a * b as f64), + (Ratio(a), Float(b)) => Ratio(a * b), + (Float(a), Ratio(b)) => Ratio(a * b), + (Int(a), Ratio(b)) => Ratio(a as f64 * b), + + (Relative(a), Int(b)) => Relative(a * b as f64), + (Relative(a), Float(b)) => Relative(a * b), + (Int(a), Relative(b)) => Relative(a as f64 * b), + (Float(a), Relative(b)) => Relative(a * b), + + (Float(a), Fraction(b)) => Fraction(a * b), + (Fraction(a), Int(b)) => Fraction(a * b as f64), + (Fraction(a), Float(b)) => Fraction(a * b), + (Int(a), Fraction(b)) => Fraction(a as f64 * b), + + (Str(a), Int(b)) => Str(a.repeat(b)?), + (Int(a), Str(b)) => Str(b.repeat(a)?), + (Array(a), Int(b)) => Array(a.repeat(b)?), + (Int(a), Array(b)) => Array(b.repeat(a)?), + (Content(a), Int(b)) => Content(a.repeat(b)?), + (Int(a), Content(b)) => Content(b.repeat(a)?), + + (a, b) => mismatch!("cannot multiply {} with {}", a, b), + }) +} + +/// Compute the quotient of two values. +pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> { + if is_zero(&rhs) { + Err("cannot divide by zero")?; + } + + Ok(match (lhs, rhs) { + (Int(a), Int(b)) => Float(a as f64 / b as f64), + (Int(a), Float(b)) => Float(a as f64 / b), + (Float(a), Int(b)) => Float(a / b as f64), + (Float(a), Float(b)) => Float(a / b), + + (Length(a), Int(b)) => Length(a / b as f64), + (Length(a), Float(b)) => Length(a / b), + (Length(a), Length(b)) => Float(try_div_length(a, b)?), + (Length(a), Relative(b)) if b.rel.is_zero() => Float(try_div_length(a, b.abs)?), + + (Angle(a), Int(b)) => Angle(a / b as f64), + (Angle(a), Float(b)) => Angle(a / b), + (Angle(a), Angle(b)) => Float(a / b), + + (Ratio(a), Int(b)) => Ratio(a / b as f64), + (Ratio(a), Float(b)) => Ratio(a / b), + (Ratio(a), Ratio(b)) => Float(a / b), + (Ratio(a), Relative(b)) if b.abs.is_zero() => Float(a / b.rel), + + (Relative(a), Int(b)) => Relative(a / b as f64), + (Relative(a), Float(b)) => Relative(a / b), + (Relative(a), Length(b)) if a.rel.is_zero() => Float(try_div_length(a.abs, b)?), + (Relative(a), Ratio(b)) if a.abs.is_zero() => Float(a.rel / b), + (Relative(a), Relative(b)) => Float(try_div_relative(a, b)?), + + (Fraction(a), Int(b)) => Fraction(a / b as f64), + (Fraction(a), Float(b)) => Fraction(a / b), + (Fraction(a), Fraction(b)) => Float(a / b), + + (a, b) => mismatch!("cannot divide {} by {}", a, b), + }) +} + +/// Whether a value is a numeric zero. +fn is_zero(v: &Value) -> bool { + match *v { + Int(v) => v == 0, + Float(v) => v == 0.0, + Length(v) => v.is_zero(), + Angle(v) => v.is_zero(), + Ratio(v) => v.is_zero(), + Relative(v) => v.is_zero(), + Fraction(v) => v.is_zero(), + _ => false, + } +} + +/// Try to divide two lengths. +fn try_div_length(a: Length, b: Length) -> StrResult<f64> { + a.try_div(b).ok_or_else(|| "cannot divide these two lengths".into()) +} + +/// Try to divide two relative lengths. +fn try_div_relative(a: Rel<Length>, b: Rel<Length>) -> StrResult<f64> { + a.try_div(b) + .ok_or_else(|| "cannot divide these two relative lengths".into()) +} + +/// Compute the logical "not" of a value. +pub fn not(value: Value) -> StrResult<Value> { + match value { + Bool(b) => Ok(Bool(!b)), + v => mismatch!("cannot apply 'not' to {}", v), + } +} + +/// Compute the logical "and" of two values. +pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> { + match (lhs, rhs) { + (Bool(a), Bool(b)) => Ok(Bool(a && b)), + (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b), + } +} + +/// Compute the logical "or" of two values. +pub fn or(lhs: Value, rhs: Value) -> StrResult<Value> { + match (lhs, rhs) { + (Bool(a), Bool(b)) => Ok(Bool(a || b)), + (a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b), + } +} + +/// Compute whether two values are equal. +pub fn eq(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(Bool(equal(&lhs, &rhs))) +} + +/// Compute whether two values are unequal. +pub fn neq(lhs: Value, rhs: Value) -> StrResult<Value> { + Ok(Bool(!equal(&lhs, &rhs))) +} + +macro_rules! comparison { + ($name:ident, $op:tt, $($pat:tt)*) => { + /// Compute how a value compares with another value. + pub fn $name(lhs: Value, rhs: Value) -> StrResult<Value> { + if let Some(ordering) = compare(&lhs, &rhs) { + Ok(Bool(matches!(ordering, $($pat)*))) + } else { + mismatch!(concat!("cannot apply '", $op, "' to {} and {}"), lhs, rhs); + } + } + }; +} + +comparison!(lt, "<", Ordering::Less); +comparison!(leq, "<=", Ordering::Less | Ordering::Equal); +comparison!(gt, ">", Ordering::Greater); +comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); + +/// Determine whether two values are equal. +pub fn equal(lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + // Compare reflexively. + (None, None) => true, + (Auto, Auto) => true, + (Bool(a), Bool(b)) => a == b, + (Int(a), Int(b)) => a == b, + (Float(a), Float(b)) => a == b, + (Length(a), Length(b)) => a == b, + (Angle(a), Angle(b)) => a == b, + (Ratio(a), Ratio(b)) => a == b, + (Relative(a), Relative(b)) => a == b, + (Fraction(a), Fraction(b)) => a == b, + (Color(a), Color(b)) => a == b, + (Symbol(a), Symbol(b)) => a == b, + (Str(a), Str(b)) => a == b, + (Label(a), Label(b)) => a == b, + (Array(a), Array(b)) => a == b, + (Dict(a), Dict(b)) => a == b, + (Func(a), Func(b)) => a == b, + (Args(a), Args(b)) => a == b, + (Module(a), Module(b)) => a == b, + (Dyn(a), Dyn(b)) => a == b, + + // Some technically different things should compare equal. + (&Int(a), &Float(b)) => a as f64 == b, + (&Float(a), &Int(b)) => a == b as f64, + (&Length(a), &Relative(b)) => a == b.abs && b.rel.is_zero(), + (&Ratio(a), &Relative(b)) => a == b.rel && b.abs.is_zero(), + (&Relative(a), &Length(b)) => a.abs == b && a.rel.is_zero(), + (&Relative(a), &Ratio(b)) => a.rel == b && a.abs.is_zero(), + + _ => false, + } +} + +/// Compare two values. +pub fn compare(lhs: &Value, rhs: &Value) -> Option<Ordering> { + match (lhs, rhs) { + (Bool(a), Bool(b)) => a.partial_cmp(b), + (Int(a), Int(b)) => a.partial_cmp(b), + (Float(a), Float(b)) => a.partial_cmp(b), + (Length(a), Length(b)) => a.partial_cmp(b), + (Angle(a), Angle(b)) => a.partial_cmp(b), + (Ratio(a), Ratio(b)) => a.partial_cmp(b), + (Relative(a), Relative(b)) => a.partial_cmp(b), + (Fraction(a), Fraction(b)) => a.partial_cmp(b), + (Str(a), Str(b)) => a.partial_cmp(b), + + // Some technically different things should be comparable. + (&Int(a), &Float(b)) => (a as f64).partial_cmp(&b), + (&Float(a), &Int(b)) => a.partial_cmp(&(b as f64)), + (&Length(a), &Relative(b)) if b.rel.is_zero() => a.partial_cmp(&b.abs), + (&Ratio(a), &Relative(b)) if b.abs.is_zero() => a.partial_cmp(&b.rel), + (&Relative(a), &Length(b)) if a.rel.is_zero() => a.abs.partial_cmp(&b), + (&Relative(a), &Ratio(b)) if a.abs.is_zero() => a.rel.partial_cmp(&b), + + _ => Option::None, + } +} + +/// Test whether one value is "in" another one. +pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Bool(b)) + } else { + mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) + } +} + +/// Test whether one value is "not in" another one. +pub fn not_in(lhs: Value, rhs: Value) -> StrResult<Value> { + if let Some(b) = contains(&lhs, &rhs) { + Ok(Bool(!b)) + } else { + mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) + } +} + +/// Test for containment. +pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { + match (lhs, rhs) { + (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), + (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), + (Str(a), Dict(b)) => Some(b.contains(a)), + (a, Array(b)) => Some(b.contains(a)), + _ => Option::None, + } +} diff --git a/src/eval/scope.rs b/src/eval/scope.rs new file mode 100644 index 00000000..f6bd2164 --- /dev/null +++ b/src/eval/scope.rs @@ -0,0 +1,174 @@ +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Formatter}; +use std::hash::Hash; + +use ecow::EcoString; + +use super::{Func, FuncType, Library, Value}; +use crate::diag::StrResult; + +/// A stack of scopes. +#[derive(Debug, Default, Clone)] +pub struct Scopes<'a> { + /// The active scope. + pub top: Scope, + /// The stack of lower scopes. + pub scopes: Vec<Scope>, + /// The standard library. + pub base: Option<&'a Library>, +} + +impl<'a> Scopes<'a> { + /// Create a new, empty hierarchy of scopes. + pub fn new(base: Option<&'a Library>) -> Self { + Self { top: Scope::new(), scopes: vec![], base } + } + + /// Enter a new scope. + pub fn enter(&mut self) { + self.scopes.push(std::mem::take(&mut self.top)); + } + + /// Exit the topmost scope. + /// + /// This panics if no scope was entered. + pub fn exit(&mut self) { + self.top = self.scopes.pop().expect("no pushed scope"); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> StrResult<&Value> { + Ok(std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .chain(self.base.map(|base| base.global.scope())) + .find_map(|scope| scope.get(var)) + .ok_or("unknown variable")?) + } + + /// Try to access a variable immutably from within a math formula. + pub fn get_in_math(&self, var: &str) -> StrResult<&Value> { + Ok(std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .chain(self.base.map(|base| base.math.scope())) + .find_map(|scope| scope.get(var)) + .ok_or("unknown variable")?) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> StrResult<&mut Value> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => "cannot mutate a constant", + _ => "unknown variable", + } + })? + } +} + +/// A map from binding names to values. +#[derive(Default, Clone, Hash)] +pub struct Scope(BTreeMap<EcoString, Slot>, bool); + +impl Scope { + /// Create a new empty scope. + pub fn new() -> Self { + Self(BTreeMap::new(), false) + } + + /// Create a new scope with duplication prevention. + pub fn deduplicating() -> Self { + Self(BTreeMap::new(), true) + } + + /// Bind a value to a name. + #[track_caller] + pub fn define(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) { + let name = name.into(); + + #[cfg(debug_assertions)] + if self.1 && self.0.contains_key(&name) { + panic!("duplicate definition: {name}"); + } + + self.0.insert(name, Slot::new(value.into(), Kind::Normal)); + } + + /// Define a function through a native rust function. + pub fn def_func<T: FuncType>(&mut self, name: &'static str) { + self.define(name, Func::from_type::<T>(name)); + } + + /// Define a captured, immutable binding. + pub fn define_captured( + &mut self, + var: impl Into<EcoString>, + value: impl Into<Value>, + ) { + self.0.insert(var.into(), Slot::new(value.into(), Kind::Captured)); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> Option<&Value> { + self.0.get(var).map(Slot::read) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> Option<StrResult<&mut Value>> { + self.0.get_mut(var).map(Slot::write) + } + + /// Iterate over all definitions. + pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value)> { + self.0.iter().map(|(k, v)| (k, v.read())) + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; + f.debug_map() + .entries(self.0.iter().map(|(k, v)| (k, v.read()))) + .finish() + } +} + +/// A slot where a value is stored. +#[derive(Clone, Hash)] +struct Slot { + /// The stored value. + value: Value, + /// The kind of slot, determines how the value can be accessed. + kind: Kind, +} + +/// The different kinds of slots. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Kind { + /// A normal, mutable binding. + Normal, + /// A captured copy of another variable. + Captured, +} + +impl Slot { + /// Create a new slot. + fn new(value: Value, kind: Kind) -> Self { + Self { value, kind } + } + + /// Read the value. + fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the value. + fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + Kind::Normal => Ok(&mut self.value), + Kind::Captured => Err("cannot mutate a captured variable")?, + } + } +} diff --git a/src/eval/str.rs b/src/eval/str.rs new file mode 100644 index 00000000..63ea5dc8 --- /dev/null +++ b/src/eval/str.rs @@ -0,0 +1,514 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt::{self, Debug, Display, Formatter, Write}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, AddAssign, Deref}; + +use ecow::EcoString; +use unicode_segmentation::UnicodeSegmentation; + +use super::{castable, dict, Array, Dict, Value}; +use crate::diag::StrResult; +use crate::geom::GenAlign; + +/// Create a new [`Str`] from a format string. +#[macro_export] +#[doc(hidden)] +macro_rules! __format_str { + ($($tts:tt)*) => {{ + $crate::eval::Str::from($crate::eval::eco_format!($($tts)*)) + }}; +} + +#[doc(inline)] +pub use crate::__format_str as format_str; +#[doc(hidden)] +pub use ecow::eco_format; + +/// An immutable reference counted string. +#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Str(EcoString); + +impl Str { + /// Create a new, empty string. + pub fn new() -> Self { + Self(EcoString::new()) + } + + /// The length of the string in bytes. + pub fn len(&self) -> i64 { + self.0.len() as i64 + } + + /// A string slice containing the entire string. + pub fn as_str(&self) -> &str { + self + } + + /// Extract the first grapheme cluster. + pub fn first(&self) -> StrResult<Self> { + self.0 + .graphemes(true) + .next() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extract the last grapheme cluster. + pub fn last(&self) -> StrResult<Self> { + self.0 + .graphemes(true) + .next_back() + .map(Into::into) + .ok_or_else(string_is_empty) + } + + /// Extract the grapheme cluster at the given index. + pub fn at(&self, index: i64) -> StrResult<Self> { + let len = self.len(); + let grapheme = self.0[self.locate(index)?..] + .graphemes(true) + .next() + .ok_or_else(|| out_of_bounds(index, len))?; + Ok(grapheme.into()) + } + + /// Extract a contigous substring. + pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { + let start = self.locate(start)?; + let end = self.locate(end.unwrap_or(self.len()))?.max(start); + Ok(self.0[start..end].into()) + } + + /// The grapheme clusters the string consists of. + pub fn clusters(&self) -> Array { + self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() + } + + /// The codepoints the string consists of. + pub fn codepoints(&self) -> Array { + self.chars().map(|c| Value::Str(c.into())).collect() + } + + /// Whether the given pattern exists in this string. + pub fn contains(&self, pattern: StrPattern) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()), + StrPattern::Regex(re) => re.is_match(self), + } + } + + /// Whether this string begins with the given pattern. + pub fn starts_with(&self, pattern: StrPattern) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), + StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), + } + } + + /// Whether this string ends with the given pattern. + pub fn ends_with(&self, pattern: StrPattern) -> bool { + match pattern { + StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), + StrPattern::Regex(re) => { + re.find_iter(self).last().map_or(false, |m| m.end() == self.0.len()) + } + } + } + + /// The text of the pattern's first match in this string. + pub fn find(&self, pattern: StrPattern) -> Option<Self> { + match pattern { + StrPattern::Str(pat) => self.0.contains(pat.as_str()).then(|| pat), + StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), + } + } + + /// The position of the pattern's first match in this string. + pub fn position(&self, pattern: StrPattern) -> Option<i64> { + match pattern { + StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), + StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), + } + } + + /// The start and, text and capture groups (if any) of the first match of + /// the pattern in this string. + pub fn match_(&self, pattern: StrPattern) -> Option<Dict> { + match pattern { + StrPattern::Str(pat) => { + self.0.match_indices(pat.as_str()).next().map(match_to_dict) + } + StrPattern::Regex(re) => re.captures(self).map(captures_to_dict), + } + } + + /// The start, end, text and capture groups (if any) of all matches of the + /// pattern in this string. + pub fn matches(&self, pattern: StrPattern) -> Array { + match pattern { + StrPattern::Str(pat) => self + .0 + .match_indices(pat.as_str()) + .map(match_to_dict) + .map(Value::Dict) + .collect(), + StrPattern::Regex(re) => re + .captures_iter(self) + .map(captures_to_dict) + .map(Value::Dict) + .collect(), + } + } + + /// Split this string at whitespace or a specific pattern. + pub fn split(&self, pattern: Option<StrPattern>) -> Array { + let s = self.as_str(); + match pattern { + None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), + Some(StrPattern::Str(pat)) => { + s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() + } + Some(StrPattern::Regex(re)) => { + re.split(s).map(|v| Value::Str(v.into())).collect() + } + } + } + + /// Trim either whitespace or the given pattern at both or just one side of + /// the string. If `repeat` is true, the pattern is trimmed repeatedly + /// instead of just once. Repeat must only be given in combination with a + /// pattern. + pub fn trim( + &self, + pattern: Option<StrPattern>, + at: Option<StrSide>, + repeat: bool, + ) -> Self { + let mut start = matches!(at, Some(StrSide::Start) | None); + let end = matches!(at, Some(StrSide::End) | None); + + let trimmed = match pattern { + None => match at { + None => self.0.trim(), + Some(StrSide::Start) => self.0.trim_start(), + Some(StrSide::End) => self.0.trim_end(), + }, + Some(StrPattern::Str(pat)) => { + let pat = pat.as_str(); + let mut s = self.as_str(); + if repeat { + if start { + s = s.trim_start_matches(pat); + } + if end { + s = s.trim_end_matches(pat); + } + } else { + if start { + s = s.strip_prefix(pat).unwrap_or(s); + } + if end { + s = s.strip_suffix(pat).unwrap_or(s); + } + } + s + } + Some(StrPattern::Regex(re)) => { + let s = self.as_str(); + let mut last = 0; + let mut range = 0..s.len(); + + for m in re.find_iter(s) { + // Does this match follow directly after the last one? + let consecutive = last == m.start(); + + // As long as we're consecutive and still trimming at the + // start, trim. + start &= consecutive; + if start { + range.start = m.end(); + start &= repeat; + } + + // Reset end trim if we aren't consecutive anymore or aren't + // repeating. + if end && (!consecutive || !repeat) { + range.end = m.start(); + } + + last = m.end(); + } + + // Is the last match directly at the end? + if last < s.len() { + range.end = s.len(); + } + + &s[range.start..range.start.max(range.end)] + } + }; + + trimmed.into() + } + + /// Replace at most `count` occurances of the given pattern with a + /// replacement string (beginning from the start). + pub fn replace(&self, pattern: StrPattern, with: Self, count: Option<usize>) -> Self { + match pattern { + StrPattern::Str(pat) => match count { + Some(n) => self.0.replacen(pat.as_str(), &with, n).into(), + None => self.0.replace(pat.as_str(), &with).into(), + }, + StrPattern::Regex(re) => match count { + Some(n) => re.replacen(self, n, with.as_str()).into(), + None => re.replace(self, with.as_str()).into(), + }, + } + } + + /// Repeat the string a number of times. + pub fn repeat(&self, n: i64) -> StrResult<Self> { + let n = usize::try_from(n) + .ok() + .and_then(|n| self.0.len().checked_mul(n).map(|_| n)) + .ok_or_else(|| format!("cannot repeat this string {} times", n))?; + + Ok(Self(self.0.repeat(n))) + } + + /// Resolve an index. + fn locate(&self, index: i64) -> StrResult<usize> { + let wrapped = + if index >= 0 { Some(index) } else { self.len().checked_add(index) }; + + let resolved = wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()) + .ok_or_else(|| out_of_bounds(index, self.len()))?; + + if !self.0.is_char_boundary(resolved) { + return Err(not_a_char_boundary(index)); + } + + Ok(resolved) + } +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: i64) -> EcoString { + eco_format!("string index out of bounds (index: {}, len: {})", index, len) +} + +/// The char boundary access error message. +#[cold] +fn not_a_char_boundary(index: i64) -> EcoString { + eco_format!("string index {} is not a character boundary", index) +} + +/// The error message when the string is empty. +#[cold] +fn string_is_empty() -> EcoString { + "string is empty".into() +} + +/// Convert an item of std's `match_indices` to a dictionary. +fn match_to_dict((start, text): (usize, &str)) -> Dict { + dict! { + "start" => Value::Int(start as i64), + "end" => Value::Int((start + text.len()) as i64), + "text" => Value::Str(text.into()), + "captures" => Value::Array(Array::new()), + } +} + +/// Convert regex captures to a dictionary. +fn captures_to_dict(cap: regex::Captures) -> Dict { + let m = cap.get(0).expect("missing first match"); + dict! { + "start" => Value::Int(m.start() as i64), + "end" => Value::Int(m.end() as i64), + "text" => Value::Str(m.as_str().into()), + "captures" => Value::Array( + cap.iter() + .skip(1) + .map(|opt| opt.map_or(Value::None, |m| m.as_str().into())) + .collect(), + ), + } +} + +impl Deref for Str { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl Display for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(self) + } +} + +impl Debug for Str { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char('"')?; + for c in self.chars() { + match c { + '\0' => f.write_str("\\u{0}")?, + '\'' => f.write_str("'")?, + '"' => f.write_str(r#"\""#)?, + _ => Display::fmt(&c.escape_debug(), f)?, + } + } + f.write_char('"') + } +} + +impl Add for Str { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Str { + fn add_assign(&mut self, rhs: Self) { + self.0.push_str(rhs.as_str()); + } +} + +impl AsRef<str> for Str { + fn as_ref(&self) -> &str { + self + } +} + +impl Borrow<str> for Str { + fn borrow(&self) -> &str { + self + } +} + +impl From<char> for Str { + fn from(c: char) -> Self { + Self(c.into()) + } +} + +impl From<&str> for Str { + fn from(s: &str) -> Self { + Self(s.into()) + } +} + +impl From<EcoString> for Str { + fn from(s: EcoString) -> Self { + Self(s) + } +} + +impl From<String> for Str { + fn from(s: String) -> Self { + Self(s.into()) + } +} + +impl From<Cow<'_, str>> for Str { + fn from(s: Cow<str>) -> Self { + Self(s.into()) + } +} + +impl FromIterator<char> for Str { + fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl From<Str> for EcoString { + fn from(str: Str) -> Self { + str.0 + } +} + +impl From<Str> for String { + fn from(s: Str) -> Self { + s.0.into() + } +} + +/// A regular expression. +#[derive(Clone)] +pub struct Regex(regex::Regex); + +impl Regex { + /// Create a new regular expression. + pub fn new(re: &str) -> StrResult<Self> { + regex::Regex::new(re).map(Self).map_err(|err| eco_format!("{err}")) + } +} + +impl Deref for Regex { + type Target = regex::Regex; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Debug for Regex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "regex({:?})", self.0.as_str()) + } +} + +impl PartialEq for Regex { + fn eq(&self, other: &Self) -> bool { + self.0.as_str() == other.0.as_str() + } +} + +impl Hash for Regex { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.as_str().hash(state); + } +} + +/// A pattern which can be searched for in a string. +#[derive(Debug, Clone)] +pub enum StrPattern { + /// Just a string. + Str(Str), + /// A regular expression. + Regex(Regex), +} + +castable! { + StrPattern, + text: Str => Self::Str(text), + regex: Regex => Self::Regex(regex), +} + +/// A side of a string. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum StrSide { + /// The logical start of the string, may be left or right depending on the + /// language. + Start, + /// The logical end of the string. + End, +} + +castable! { + StrSide, + align: GenAlign => match align { + GenAlign::Start => Self::Start, + GenAlign::End => Self::End, + _ => Err("expected either `start` or `end`")?, + }, +} diff --git a/src/eval/symbol.rs b/src/eval/symbol.rs new file mode 100644 index 00000000..73c41067 --- /dev/null +++ b/src/eval/symbol.rs @@ -0,0 +1,189 @@ +use std::cmp::Reverse; +use std::collections::BTreeSet; +use std::fmt::{self, Debug, Display, Formatter, Write}; + +use ecow::{EcoString, EcoVec}; + +use crate::diag::StrResult; + +#[doc(inline)] +pub use typst_macros::symbols; + +/// A symbol. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct Symbol { + repr: Repr, + modifiers: EcoString, +} + +/// A collection of symbols. +#[derive(Clone, Eq, PartialEq, Hash)] +enum Repr { + Single(char), + Static(&'static [(&'static str, char)]), + Runtime(EcoVec<(EcoString, char)>), +} + +impl Symbol { + /// Create a new symbol from a single character. + pub const fn new(c: char) -> Self { + Self { repr: Repr::Single(c), modifiers: EcoString::new() } + } + + /// Create a symbol with a static variant list. + #[track_caller] + pub const fn list(list: &'static [(&'static str, char)]) -> Self { + debug_assert!(!list.is_empty()); + Self { + repr: Repr::Static(list), + modifiers: EcoString::new(), + } + } + + /// Create a symbol with a runtime variant list. + #[track_caller] + pub fn runtime(list: EcoVec<(EcoString, char)>) -> Self { + debug_assert!(!list.is_empty()); + Self { + repr: Repr::Runtime(list), + modifiers: EcoString::new(), + } + } + + /// Get the symbol's text. + pub fn get(&self) -> char { + match self.repr { + Repr::Single(c) => c, + _ => find(self.variants(), &self.modifiers).unwrap(), + } + } + + /// Apply a modifier to the symbol. + pub fn modified(mut self, modifier: &str) -> StrResult<Self> { + if !self.modifiers.is_empty() { + self.modifiers.push('.'); + } + self.modifiers.push_str(modifier); + if find(self.variants(), &self.modifiers).is_none() { + Err("unknown modifier")? + } + Ok(self) + } + + /// The characters that are covered by this symbol. + pub fn variants(&self) -> impl Iterator<Item = (&str, char)> { + match &self.repr { + Repr::Single(c) => Variants::Single(Some(*c).into_iter()), + Repr::Static(list) => Variants::Static(list.iter()), + Repr::Runtime(list) => Variants::Runtime(list.iter()), + } + } + + /// Possible modifiers. + pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ { + let mut set = BTreeSet::new(); + for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { + if !modifier.is_empty() && !contained(&self.modifiers, modifier) { + set.insert(modifier); + } + } + set.into_iter() + } +} + +impl Debug for Symbol { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char(self.get()) + } +} + +impl Display for Symbol { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_char(self.get()) + } +} + +/// Iterator over variants. +enum Variants<'a> { + Single(std::option::IntoIter<char>), + Static(std::slice::Iter<'static, (&'static str, char)>), + Runtime(std::slice::Iter<'a, (EcoString, char)>), +} + +impl<'a> Iterator for Variants<'a> { + type Item = (&'a str, char); + + fn next(&mut self) -> Option<Self::Item> { + match self { + Self::Single(iter) => Some(("", iter.next()?)), + Self::Static(list) => list.next().copied(), + Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)), + } + } +} + +/// Find the best symbol from the list. +fn find<'a>( + variants: impl Iterator<Item = (&'a str, char)>, + modifiers: &str, +) -> Option<char> { + let mut best = None; + let mut best_score = None; + + // Find the best table entry with this name. + 'outer: for candidate in variants { + for modifier in parts(modifiers) { + if !contained(candidate.0, modifier) { + continue 'outer; + } + } + + let mut matching = 0; + let mut total = 0; + for modifier in parts(candidate.0) { + if contained(modifiers, modifier) { + matching += 1; + } + total += 1; + } + + let score = (matching, Reverse(total)); + if best_score.map_or(true, |b| score > b) { + best = Some(candidate.1); + best_score = Some(score); + } + } + + best +} + +/// Split a modifier list into its parts. +fn parts(modifiers: &str) -> impl Iterator<Item = &str> { + modifiers.split('.').filter(|s| !s.is_empty()) +} + +/// Whether the modifier string contains the modifier `m`. +fn contained(modifiers: &str, m: &str) -> bool { + parts(modifiers).any(|part| part == m) +} + +/// Normalize an accent to a combining one. +pub fn combining_accent(c: char) -> Option<char> { + Some(match c { + '\u{0300}' | '`' => '\u{0300}', + '\u{0301}' | '´' => '\u{0301}', + '\u{0302}' | '^' | 'ˆ' => '\u{0302}', + '\u{0303}' | '~' | '∼' | '˜' => '\u{0303}', + '\u{0304}' | '¯' => '\u{0304}', + '\u{0305}' | '-' | '‾' | '−' => '\u{0305}', + '\u{0306}' | '˘' => '\u{0306}', + '\u{0307}' | '.' | '˙' | '⋅' => '\u{0307}', + '\u{0308}' | '¨' => '\u{0308}', + '\u{030a}' | '∘' | '○' => '\u{030a}', + '\u{030b}' | '˝' => '\u{030b}', + '\u{030c}' | 'ˇ' => '\u{030c}', + '\u{20d6}' | '←' => '\u{20d6}', + '\u{20d7}' | '→' | '⟶' => '\u{20d7}', + _ => return None, + }) +} diff --git a/src/eval/value.rs b/src/eval/value.rs new file mode 100644 index 00000000..5e06da76 --- /dev/null +++ b/src/eval/value.rs @@ -0,0 +1,497 @@ +use std::any::Any; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use siphasher::sip128::{Hasher128, SipHasher}; + +use super::{ + format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Module, + Str, Symbol, +}; +use crate::diag::StrResult; +use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; +use crate::syntax::{ast, Span}; + +/// A computational value. +#[derive(Clone)] +pub enum Value { + /// The value that indicates the absence of a meaningful value. + None, + /// A value that indicates some smart default behaviour. + Auto, + /// A boolean: `true, false`. + Bool(bool), + /// An integer: `120`. + Int(i64), + /// A floating-point number: `1.2`, `10e-4`. + Float(f64), + /// A length: `12pt`, `3cm`, `1.5em`, `1em - 2pt`. + Length(Length), + /// An angle: `1.5rad`, `90deg`. + Angle(Angle), + /// A ratio: `50%`. + Ratio(Ratio), + /// A relative length, combination of a ratio and a length: `20% + 5cm`. + Relative(Rel<Length>), + /// A fraction: `1fr`. + Fraction(Fr), + /// A color value: `#f79143ff`. + Color(Color), + /// A symbol: `arrow.l`. + Symbol(Symbol), + /// A string: `"string"`. + Str(Str), + /// A label: `<intro>`. + Label(Label), + /// A content value: `[*Hi* there]`. + Content(Content), + /// An array of values: `(1, "hi", 12cm)`. + Array(Array), + /// A dictionary value: `(color: #f79143, pattern: dashed)`. + Dict(Dict), + /// An executable function. + Func(Func), + /// Captured arguments to a function. + Args(Args), + /// A module. + Module(Module), + /// A dynamic value. + Dyn(Dynamic), +} + +impl Value { + /// Create a new dynamic value. + pub fn dynamic<T>(any: T) -> Self + where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + { + Self::Dyn(Dynamic::new(any)) + } + + /// Create a numeric value from a number with a unit. + pub fn numeric(pair: (f64, ast::Unit)) -> Self { + let (v, unit) = pair; + match unit { + ast::Unit::Length(unit) => Abs::with_unit(v, unit).into(), + ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into(), + ast::Unit::Em => Em::new(v).into(), + ast::Unit::Fr => Fr::new(v).into(), + ast::Unit::Percent => Ratio::new(v / 100.0).into(), + } + } + + /// The name of the stored value's type. + pub fn type_name(&self) -> &'static str { + match self { + Self::None => "none", + Self::Auto => "auto", + Self::Bool(_) => bool::TYPE_NAME, + Self::Int(_) => i64::TYPE_NAME, + Self::Float(_) => f64::TYPE_NAME, + Self::Length(_) => Length::TYPE_NAME, + Self::Angle(_) => Angle::TYPE_NAME, + Self::Ratio(_) => Ratio::TYPE_NAME, + Self::Relative(_) => Rel::<Length>::TYPE_NAME, + Self::Fraction(_) => Fr::TYPE_NAME, + Self::Color(_) => Color::TYPE_NAME, + Self::Symbol(_) => Symbol::TYPE_NAME, + Self::Str(_) => Str::TYPE_NAME, + Self::Label(_) => Label::TYPE_NAME, + Self::Content(_) => Content::TYPE_NAME, + Self::Array(_) => Array::TYPE_NAME, + Self::Dict(_) => Dict::TYPE_NAME, + Self::Func(_) => Func::TYPE_NAME, + Self::Args(_) => Args::TYPE_NAME, + Self::Module(_) => Module::TYPE_NAME, + Self::Dyn(v) => v.type_name(), + } + } + + /// Try to cast the value into a specific type. + pub fn cast<T: Cast>(self) -> StrResult<T> { + T::cast(self) + } + + /// Try to access a field on the value. + pub fn field(&self, field: &str) -> StrResult<Value> { + match self { + Self::Symbol(symbol) => symbol.clone().modified(&field).map(Self::Symbol), + Self::Dict(dict) => dict.at(&field).cloned(), + Self::Content(content) => content + .field(&field) + .ok_or_else(|| eco_format!("unknown field `{field}`")), + Self::Module(module) => module.get(&field).cloned(), + v => Err(eco_format!("cannot access fields on type {}", v.type_name())), + } + } + + /// Return the debug representation of the value. + pub fn repr(&self) -> Str { + format_str!("{:?}", self) + } + + /// Attach a span to the value, if possible. + pub fn spanned(self, span: Span) -> Self { + match self { + Value::Content(v) => Value::Content(v.spanned(span)), + Value::Func(v) => Value::Func(v.spanned(span)), + v => v, + } + } + + /// Return the display representation of the value. + pub fn display(self) -> Content { + match self { + Self::None => Content::empty(), + Self::Int(v) => item!(text)(eco_format!("{}", v)), + Self::Float(v) => item!(text)(eco_format!("{}", v)), + Self::Str(v) => item!(text)(v.into()), + Self::Symbol(v) => item!(text)(v.get().into()), + Self::Content(v) => v, + Self::Func(_) => Content::empty(), + Self::Module(module) => module.content(), + _ => item!(raw)(self.repr().into(), Some("typc".into()), false), + } + } + + /// Try to extract documentation for the value. + pub fn docs(&self) -> Option<&'static str> { + match self { + Self::Func(func) => func.info().map(|info| info.docs), + _ => None, + } + } +} + +impl Default for Value { + fn default() -> Self { + Value::None + } +} + +impl Debug for Value { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::None => f.pad("none"), + Self::Auto => f.pad("auto"), + Self::Bool(v) => Debug::fmt(v, f), + Self::Int(v) => Debug::fmt(v, f), + Self::Float(v) => Debug::fmt(v, f), + Self::Length(v) => Debug::fmt(v, f), + Self::Angle(v) => Debug::fmt(v, f), + Self::Ratio(v) => Debug::fmt(v, f), + Self::Relative(v) => Debug::fmt(v, f), + Self::Fraction(v) => Debug::fmt(v, f), + Self::Color(v) => Debug::fmt(v, f), + Self::Symbol(v) => Debug::fmt(v, f), + Self::Str(v) => Debug::fmt(v, f), + Self::Label(v) => Debug::fmt(v, f), + Self::Content(_) => f.pad("[...]"), + Self::Array(v) => Debug::fmt(v, f), + Self::Dict(v) => Debug::fmt(v, f), + Self::Func(v) => Debug::fmt(v, f), + Self::Args(v) => Debug::fmt(v, f), + Self::Module(v) => Debug::fmt(v, f), + Self::Dyn(v) => Debug::fmt(v, f), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + ops::equal(self, other) + } +} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + ops::compare(self, other) + } +} + +impl Hash for Value { + fn hash<H: Hasher>(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::None => {} + Self::Auto => {} + Self::Bool(v) => v.hash(state), + Self::Int(v) => v.hash(state), + Self::Float(v) => v.to_bits().hash(state), + Self::Length(v) => v.hash(state), + Self::Angle(v) => v.hash(state), + Self::Ratio(v) => v.hash(state), + Self::Relative(v) => v.hash(state), + Self::Fraction(v) => v.hash(state), + Self::Color(v) => v.hash(state), + Self::Symbol(v) => v.hash(state), + Self::Str(v) => v.hash(state), + Self::Label(v) => v.hash(state), + Self::Content(v) => v.hash(state), + Self::Array(v) => v.hash(state), + Self::Dict(v) => v.hash(state), + Self::Func(v) => v.hash(state), + Self::Args(v) => v.hash(state), + Self::Module(v) => v.hash(state), + Self::Dyn(v) => v.hash(state), + } + } +} + +impl From<i32> for Value { + fn from(v: i32) -> Self { + Self::Int(v as i64) + } +} + +impl From<usize> for Value { + fn from(v: usize) -> Self { + Self::Int(v as i64) + } +} + +impl From<Abs> for Value { + fn from(v: Abs) -> Self { + Self::Length(v.into()) + } +} + +impl From<Em> for Value { + fn from(v: Em) -> Self { + Self::Length(v.into()) + } +} + +impl From<RgbaColor> for Value { + fn from(v: RgbaColor) -> Self { + Self::Color(v.into()) + } +} + +impl From<&str> for Value { + fn from(v: &str) -> Self { + Self::Str(v.into()) + } +} + +impl From<EcoString> for Value { + fn from(v: EcoString) -> Self { + Self::Str(v.into()) + } +} + +impl From<String> for Value { + fn from(v: String) -> Self { + Self::Str(v.into()) + } +} + +impl From<Dynamic> for Value { + fn from(v: Dynamic) -> Self { + Self::Dyn(v) + } +} + +/// A dynamic value. +#[derive(Clone, Hash)] +pub struct Dynamic(Arc<dyn Bounds>); + +impl Dynamic { + /// Create a new instance from any value that satisifies the required bounds. + pub fn new<T>(any: T) -> Self + where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + { + Self(Arc::new(any)) + } + + /// Whether the wrapped type is `T`. + pub fn is<T: Type + 'static>(&self) -> bool { + (*self.0).as_any().is::<T>() + } + + /// Try to downcast to a reference to a specific type. + pub fn downcast<T: Type + 'static>(&self) -> Option<&T> { + (*self.0).as_any().downcast_ref() + } + + /// The name of the stored value's type. + pub fn type_name(&self) -> &'static str { + self.0.dyn_type_name() + } +} + +impl Debug for Dynamic { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl PartialEq for Dynamic { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other) + } +} + +trait Bounds: Debug + Sync + Send + 'static { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &Dynamic) -> bool; + fn dyn_type_name(&self) -> &'static str; + fn hash128(&self) -> u128; +} + +impl<T> Bounds for T +where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, other: &Dynamic) -> bool { + let Some(other) = other.downcast::<Self>() else { return false }; + self == other + } + + fn dyn_type_name(&self) -> &'static str { + T::TYPE_NAME + } + + fn hash128(&self) -> u128 { + // Also hash the TypeId since values with different types but + // equal data should be different. + let mut state = SipHasher::new(); + self.type_id().hash(&mut state); + self.hash(&mut state); + state.finish128().as_u128() + } +} + +impl Hash for dyn Bounds { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_u128(self.hash128()); + } +} + +/// The type of a value. +pub trait Type { + /// The name of the type. + const TYPE_NAME: &'static str; +} + +/// Implement traits for primitives. +macro_rules! primitive { + ( + $type:ty: $name:literal, $variant:ident + $(, $other:ident$(($binding:ident))? => $out:expr)* + ) => { + impl Type for $type { + const TYPE_NAME: &'static str = $name; + } + + impl Cast for $type { + fn is(value: &Value) -> bool { + matches!(value, Value::$variant(_) + $(| primitive!(@$other $(($binding))?))*) + } + + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::$variant(v) => Ok(v), + $(Value::$other$(($binding))? => Ok($out),)* + v => Err(eco_format!( + "expected {}, found {}", + Self::TYPE_NAME, + v.type_name(), + )), + } + } + + fn describe() -> CastInfo { + CastInfo::Type(Self::TYPE_NAME) + } + } + + impl From<$type> for Value { + fn from(v: $type) -> Self { + Value::$variant(v) + } + } + }; + + (@$other:ident($binding:ident)) => { Value::$other(_) }; + (@$other:ident) => { Value::$other }; +} + +primitive! { bool: "boolean", Bool } +primitive! { i64: "integer", Int } +primitive! { f64: "float", Float, Int(v) => v as f64 } +primitive! { Length: "length", Length } +primitive! { Angle: "angle", Angle } +primitive! { Ratio: "ratio", Ratio } +primitive! { Rel<Length>: "relative length", + Relative, + Length(v) => v.into(), + Ratio(v) => v.into() +} +primitive! { Fr: "fraction", Fraction } +primitive! { Color: "color", Color } +primitive! { Symbol: "symbol", Symbol } +primitive! { + Str: "string", + Str, + Symbol(symbol) => symbol.get().into() +} +primitive! { Label: "label", Label } +primitive! { Content: "content", + Content, + None => Content::empty(), + Symbol(v) => item!(text)(v.get().into()), + Str(v) => item!(text)(v.into()) +} +primitive! { Array: "array", Array } +primitive! { Dict: "dictionary", Dict } +primitive! { Func: "function", Func } +primitive! { Module: "module", Module } +primitive! { Args: "arguments", Args } + +#[cfg(test)] +mod tests { + use super::*; + use crate::eval::{array, dict}; + + #[track_caller] + fn test(value: impl Into<Value>, exp: &str) { + assert_eq!(format!("{:?}", value.into()), exp); + } + + #[test] + fn test_value_debug() { + // Primitives. + test(Value::None, "none"); + test(false, "false"); + test(12i64, "12"); + test(3.14, "3.14"); + test(Abs::pt(5.5), "5.5pt"); + test(Angle::deg(90.0), "90deg"); + test(Ratio::one() / 2.0, "50%"); + test(Ratio::new(0.3) + Length::from(Abs::cm(2.0)), "30% + 56.69pt"); + test(Fr::one() * 7.55, "7.55fr"); + test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "rgb(\"#010101\")"); + + // Collections. + test("hello", r#""hello""#); + test("\n", r#""\n""#); + test("\\", r#""\\""#); + test("\"", r#""\"""#); + test(array![], "()"); + test(array![Value::None], "(none,)"); + test(array![1, 2], "(1, 2)"); + test(dict![], "(:)"); + test(dict!["one" => 1], "(one: 1)"); + test(dict!["two" => false, "one" => 1], "(one: 1, two: false)"); + } +} |
