summaryrefslogtreecommitdiff
path: root/src/eval
diff options
context:
space:
mode:
Diffstat (limited to 'src/eval')
-rw-r--r--src/eval/args.rs193
-rw-r--r--src/eval/array.rs406
-rw-r--r--src/eval/cast.rs513
-rw-r--r--src/eval/dict.rs209
-rw-r--r--src/eval/func.rs574
-rw-r--r--src/eval/library.rs145
-rw-r--r--src/eval/methods.rs276
-rw-r--r--src/eval/mod.rs1554
-rw-r--r--src/eval/module.rs87
-rw-r--r--src/eval/ops.rs414
-rw-r--r--src/eval/scope.rs174
-rw-r--r--src/eval/str.rs514
-rw-r--r--src/eval/symbol.rs189
-rw-r--r--src/eval/value.rs497
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)");
+ }
+}