summaryrefslogtreecommitdiff
path: root/src/model
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-10-17 19:26:24 +0200
committerLaurenz <laurmaedje@gmail.com>2022-10-17 20:04:22 +0200
commite21822665591dc19766275da1e185215a6b945ef (patch)
tree7788e211c3c33c8b5a8ad7d5eb7574e33631eb16 /src/model
parent4fd031a256b2ecfe524859d5599fafb386395572 (diff)
Merge some modules
Diffstat (limited to 'src/model')
-rw-r--r--src/model/args.rs225
-rw-r--r--src/model/array.rs369
-rw-r--r--src/model/capture.rs186
-rw-r--r--src/model/cast.rs360
-rw-r--r--src/model/content.rs31
-rw-r--r--src/model/dict.rs193
-rw-r--r--src/model/eval.rs1212
-rw-r--r--src/model/func.rs249
-rw-r--r--src/model/layout.rs17
-rw-r--r--src/model/methods.rs167
-rw-r--r--src/model/mod.rs34
-rw-r--r--src/model/ops.rs398
-rw-r--r--src/model/property.rs3
-rw-r--r--src/model/raw.rs293
-rw-r--r--src/model/recipe.rs6
-rw-r--r--src/model/scope.rs161
-rw-r--r--src/model/show.rs3
-rw-r--r--src/model/str.rs470
-rw-r--r--src/model/styles.rs2
-rw-r--r--src/model/value.rs455
-rw-r--r--src/model/vm.rs86
21 files changed, 4890 insertions, 30 deletions
diff --git a/src/model/args.rs b/src/model/args.rs
new file mode 100644
index 00000000..f95fbf08
--- /dev/null
+++ b/src/model/args.rs
@@ -0,0 +1,225 @@
+use std::fmt::{self, Debug, Formatter, Write};
+
+use super::{Array, Cast, Dict, Str, Value};
+use crate::diag::{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: Vec<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_positional(&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()
+ }
+
+ /// Reinterpret these arguments as actually being an array index.
+ pub fn into_index(self) -> SourceResult<i64> {
+ self.into_castable("index")
+ }
+
+ /// Reinterpret these arguments as actually being a dictionary key.
+ pub fn into_key(self) -> SourceResult<Str> {
+ self.into_castable("key")
+ }
+
+ /// Reinterpret these arguments as actually being a single castable thing.
+ fn into_castable<T: Cast>(self, what: &str) -> SourceResult<T> {
+ let mut iter = self.items.into_iter();
+ let value = match iter.next() {
+ Some(Arg { name: None, value, .. }) => value.v.cast().at(value.span)?,
+ None => {
+ bail!(self.span, "missing {}", what);
+ }
+ Some(Arg { name: Some(_), span, .. }) => {
+ bail!(span, "named pair is not allowed here");
+ }
+ };
+
+ if let Some(arg) = iter.next() {
+ bail!(arg.span, "only one {} is allowed", what);
+ }
+
+ Ok(value)
+ }
+}
+
+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/model/array.rs b/src/model/array.rs
new file mode 100644
index 00000000..196f02ec
--- /dev/null
+++ b/src/model/array.rs
@@ -0,0 +1,369 @@
+use std::cmp::Ordering;
+use std::fmt::{self, Debug, Formatter, Write};
+use std::ops::{Add, AddAssign};
+use std::sync::Arc;
+
+use super::{ops, Args, Func, Value, Vm};
+use crate::diag::{At, SourceResult, StrResult};
+use crate::syntax::Spanned;
+use crate::util::ArcExt;
+
+/// Create a new [`Array`] from values.
+#[allow(unused_macros)]
+macro_rules! array {
+ ($value:expr; $count:expr) => {
+ $crate::model::Array::from_vec(vec![$value.into(); $count])
+ };
+
+ ($($value:expr),* $(,)?) => {
+ $crate::model::Array::from_vec(vec![$($value.into()),*])
+ };
+}
+
+/// A reference counted array with value semantics.
+#[derive(Default, Clone, PartialEq, Hash)]
+pub struct Array(Arc<Vec<Value>>);
+
+impl Array {
+ /// Create a new, empty array.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Create a new array from a vector of values.
+ pub fn from_vec(vec: Vec<Value>) -> Self {
+ Self(Arc::new(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) -> Option<&Value> {
+ self.0.first()
+ }
+
+ /// The last value in the array.
+ pub fn last(&self) -> Option<&Value> {
+ self.0.last()
+ }
+
+ /// Borrow the value at the given index.
+ pub fn get(&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 get_mut(&mut self, index: i64) -> StrResult<&mut Value> {
+ let len = self.len();
+ self.locate(index)
+ .and_then(move |i| Arc::make_mut(&mut self.0).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) {
+ Arc::make_mut(&mut self.0).push(value);
+ }
+
+ /// Remove the last value in the array.
+ pub fn pop(&mut self) -> StrResult<()> {
+ Arc::make_mut(&mut self.0).pop().ok_or_else(array_is_empty)?;
+ Ok(())
+ }
+
+ /// 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))?;
+
+ Arc::make_mut(&mut self.0).insert(i, value);
+ Ok(())
+ }
+
+ /// Remove and return the value at the specified index.
+ pub fn remove(&mut self, index: i64) -> StrResult<()> {
+ let len = self.len();
+ let i = self
+ .locate(index)
+ .filter(|&i| i < self.0.len())
+ .ok_or_else(|| out_of_bounds(index, len))?;
+
+ Arc::make_mut(&mut self.0).remove(i);
+ return Ok(());
+ }
+
+ /// 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].to_vec()))
+ }
+
+ /// 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, f: Spanned<Func>) -> SourceResult<Option<Value>> {
+ for item in self.iter() {
+ let args = Args::new(f.span, [item.clone()]);
+ if f.v.call(vm, args)?.cast::<bool>().at(f.span)? {
+ return Ok(Some(item.clone()));
+ }
+ }
+
+ Ok(None)
+ }
+
+ /// Return the index of the first matching element.
+ pub fn position(&self, vm: &mut Vm, f: Spanned<Func>) -> SourceResult<Option<i64>> {
+ for (i, item) in self.iter().enumerate() {
+ let args = Args::new(f.span, [item.clone()]);
+ if f.v.call(vm, args)?.cast::<bool>().at(f.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, f: Spanned<Func>) -> SourceResult<Self> {
+ let mut kept = vec![];
+ for item in self.iter() {
+ let args = Args::new(f.span, [item.clone()]);
+ if f.v.call(vm, args)?.cast::<bool>().at(f.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, f: Spanned<Func>) -> SourceResult<Self> {
+ let enumerate = f.v.argc() == Some(2);
+ self.iter()
+ .enumerate()
+ .map(|(i, item)| {
+ let mut args = Args::new(f.span, []);
+ if enumerate {
+ args.push(f.span, Value::Int(i as i64));
+ }
+ args.push(f.span, item.clone());
+ f.v.call(vm, args)
+ })
+ .collect()
+ }
+
+ /// Whether any element matches.
+ pub fn any(&self, vm: &mut Vm, f: Spanned<Func>) -> SourceResult<bool> {
+ for item in self.iter() {
+ let args = Args::new(f.span, [item.clone()]);
+ if f.v.call(vm, args)?.cast::<bool>().at(f.span)? {
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+ }
+
+ /// Whether all elements match.
+ pub fn all(&self, vm: &mut Vm, f: Spanned<Func>) -> SourceResult<bool> {
+ for item in self.iter() {
+ let args = Args::new(f.span, [item.clone()]);
+ if !f.v.call(vm, args)?.cast::<bool>().at(f.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 = Vec::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.sort_by(|a, b| {
+ a.partial_cmp(b).unwrap_or_else(|| {
+ if result.is_ok() {
+ result = Err(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) -> String {
+ format!("array index out of bounds (index: {}, len: {})", index, len)
+}
+
+/// The error message when the array is empty.
+#[cold]
+fn array_is_empty() -> String {
+ "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) {
+ match Arc::try_unwrap(rhs.0) {
+ Ok(vec) => self.extend(vec),
+ Err(rc) => self.extend(rc.iter().cloned()),
+ }
+ }
+}
+
+impl Extend<Value> for Array {
+ fn extend<T: IntoIterator<Item = Value>>(&mut self, iter: T) {
+ Arc::make_mut(&mut self.0).extend(iter);
+ }
+}
+
+impl FromIterator<Value> for Array {
+ fn from_iter<T: IntoIterator<Item = Value>>(iter: T) -> Self {
+ Self(Arc::new(iter.into_iter().collect()))
+ }
+}
+
+impl IntoIterator for Array {
+ type Item = Value;
+ type IntoIter = std::vec::IntoIter<Value>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ Arc::take(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/model/capture.rs b/src/model/capture.rs
new file mode 100644
index 00000000..c4c107b2
--- /dev/null
+++ b/src/model/capture.rs
@@ -0,0 +1,186 @@
+use super::{Scope, Scopes, Value};
+use crate::syntax::ast::TypedNode;
+use crate::syntax::{ast, SyntaxNode};
+
+/// A visitor that captures variable slots.
+pub 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
+ }
+
+ /// Bind a new internal variable.
+ pub fn bind(&mut self, ident: ast::Ident) {
+ self.internal.top.define(ident.take(), Value::None);
+ }
+
+ /// Capture a variable if it isn't internal.
+ pub 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());
+ }
+ }
+ }
+
+ /// 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),
+
+ // 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());
+ }
+ }
+
+ 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());
+ }
+
+ // 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 show rule contains a binding, but that binding is only active
+ // after the target has been evaluated.
+ Some(ast::Expr::Show(show)) => {
+ self.visit(show.pattern().as_untyped());
+ if let Some(binding) = show.binding() {
+ self.bind(binding);
+ }
+ self.visit(show.body().as_untyped());
+ }
+
+ // 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());
+ let pattern = expr.pattern();
+ if let Some(key) = pattern.key() {
+ self.bind(key);
+ }
+ self.bind(pattern.value());
+ self.visit(expr.body().as_untyped());
+ }
+
+ // An import contains items, but these are active only after the
+ // path is evaluated.
+ Some(ast::Expr::Import(expr)) => {
+ self.visit(expr.path().as_untyped());
+ if let 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);
+ }
+ }
+ }
+ }
+}
+
+#[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("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", &[]);
+
+ // 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"]);
+
+ // Show rule.
+ test("#show x: y as x", &["y"]);
+ test("#show x: y as x + z", &["y", "z"]);
+ test("#show x: x as x", &["x"]);
+
+ // For loop.
+ test("#for x in y { x + z }", &["y", "z"]);
+ test("#for x, y in y { x + y }", &["y"]);
+
+ // Import.
+ test("#import x, y from z", &["z"]);
+ test("#import x, y, z from x + y", &["x", "y"]);
+
+ // Blocks.
+ test("{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
+ test("[#let x = 1]#x", &["x"]);
+ }
+}
diff --git a/src/model/cast.rs b/src/model/cast.rs
new file mode 100644
index 00000000..00a3fe45
--- /dev/null
+++ b/src/model/cast.rs
@@ -0,0 +1,360 @@
+use std::num::NonZeroUsize;
+
+use super::{Content, Layout, LayoutNode, Pattern, Regex, Value};
+use crate::diag::{with_alternative, StrResult};
+use crate::geom::{Corners, Dir, Paint, Sides};
+use crate::syntax::Spanned;
+use crate::util::EcoString;
+
+/// 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>;
+}
+
+/// Implement traits for dynamic types.
+macro_rules! dynamic {
+ ($type:ty: $name:literal, $($tts:tt)*) => {
+ impl $crate::model::Type for $type {
+ const TYPE_NAME: &'static str = $name;
+ }
+
+ castable! {
+ $type,
+ Expected: <Self as $crate::model::Type>::TYPE_NAME,
+ $($tts)*
+ @this: Self => this.clone(),
+ }
+
+ impl From<$type> for $crate::model::Value {
+ fn from(v: $type) -> Self {
+ $crate::model::Value::Dyn($crate::model::Dynamic::new(v))
+ }
+ }
+ };
+}
+
+/// Make a type castable from a value.
+macro_rules! castable {
+ ($type:ty: $inner:ty) => {
+ impl $crate::model::Cast<$crate::model::Value> for $type {
+ fn is(value: &$crate::model::Value) -> bool {
+ <$inner>::is(value)
+ }
+
+ fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> {
+ <$inner>::cast(value).map(Self)
+ }
+ }
+ };
+
+ (
+ $type:ty,
+ Expected: $expected:expr,
+ $($pattern:pat => $out:expr,)*
+ $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)*
+ ) => {
+ #[allow(unreachable_patterns)]
+ impl $crate::model::Cast<$crate::model::Value> for $type {
+ fn is(value: &$crate::model::Value) -> bool {
+ #[allow(unused_variables)]
+ match value {
+ $($pattern => true,)*
+ $crate::model::Value::Dyn(dynamic) => {
+ false $(|| dynamic.is::<$dyn_type>())*
+ }
+ _ => false,
+ }
+ }
+
+ fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> {
+ let found = match value {
+ $($pattern => return Ok($out),)*
+ $crate::model::Value::Dyn(dynamic) => {
+ $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() {
+ return Ok($dyn_out);
+ })*
+ dynamic.type_name()
+ }
+ v => v.type_name(),
+ };
+
+ Err(format!("expected {}, found {}", $expected, found))
+ }
+ }
+ };
+}
+
+impl Cast for Value {
+ fn is(_: &Value) -> bool {
+ true
+ }
+
+ fn cast(value: Value) -> StrResult<Self> {
+ Ok(value)
+ }
+}
+
+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)
+ }
+}
+
+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))
+ }
+}
+
+dynamic! {
+ Dir: "direction",
+}
+
+dynamic! {
+ Regex: "regular expression",
+}
+
+castable! {
+ usize,
+ Expected: "non-negative integer",
+ Value::Int(int) => int.try_into().map_err(|_| {
+ if int < 0 {
+ "must be at least zero"
+ } else {
+ "number too large"
+ }
+ })?,
+}
+
+castable! {
+ NonZeroUsize,
+ Expected: "positive integer",
+ Value::Int(int) => int
+ .try_into()
+ .and_then(|int: usize| int.try_into())
+ .map_err(|_| if int <= 0 {
+ "must be positive"
+ } else {
+ "number too large"
+ })?,
+}
+
+castable! {
+ Paint,
+ Expected: "color",
+ Value::Color(color) => Paint::Solid(color),
+}
+
+castable! {
+ EcoString,
+ Expected: "string",
+ Value::Str(str) => str.into(),
+}
+
+castable! {
+ String,
+ Expected: "string",
+ Value::Str(string) => string.into(),
+}
+
+castable! {
+ LayoutNode,
+ Expected: "content",
+ Value::None => Self::default(),
+ Value::Str(text) => Content::Text(text.into()).pack(),
+ Value::Content(content) => content.pack(),
+}
+
+castable! {
+ Pattern,
+ Expected: "function, string or regular expression",
+ Value::Func(func) => Self::Node(func.node()?),
+ Value::Str(text) => Self::text(&text),
+ @regex: Regex => Self::Regex(regex.clone()),
+}
+
+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 => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")),
+ }
+ }
+}
+
+/// A value that can be automatically determined.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum Smart<T> {
+ /// The value should be determined smartly based on the circumstances.
+ Auto,
+ /// A specific value.
+ Custom(T),
+}
+
+impl<T> Smart<T> {
+ /// Map the contained custom value with `f`.
+ pub fn map<F, U>(self, f: F) -> Smart<U>
+ where
+ F: FnOnce(T) -> U,
+ {
+ match self {
+ Self::Auto => Smart::Auto,
+ Self::Custom(x) => Smart::Custom(f(x)),
+ }
+ }
+
+ /// Keeps `self` if it contains a custom value, otherwise returns `other`.
+ pub fn or(self, other: Smart<T>) -> Self {
+ match self {
+ Self::Custom(x) => Self::Custom(x),
+ Self::Auto => other,
+ }
+ }
+
+ /// Returns the contained custom value or a provided default value.
+ pub fn unwrap_or(self, default: T) -> T {
+ match self {
+ Self::Auto => default,
+ Self::Custom(x) => x,
+ }
+ }
+
+ /// Returns the contained custom value or computes a default value.
+ pub fn unwrap_or_else<F>(self, f: F) -> T
+ where
+ F: FnOnce() -> T,
+ {
+ match self {
+ Self::Auto => f(),
+ Self::Custom(x) => x,
+ }
+ }
+
+ /// Returns the contained custom value or the default value.
+ pub fn unwrap_or_default(self) -> T
+ where
+ T: Default,
+ {
+ self.unwrap_or_else(T::default)
+ }
+}
+
+impl<T> Default for Smart<T> {
+ fn default() -> Self {
+ Self::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 => T::cast(v)
+ .map(Self::Custom)
+ .map_err(|msg| with_alternative(msg, "auto")),
+ }
+ }
+}
+
+impl<T> Cast for Sides<T>
+where
+ T: Cast + Default + 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).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),
+ };
+
+ if let Some((key, _)) = dict.iter().next() {
+ return Err(format!("unexpected key {key:?}"));
+ }
+
+ Ok(sides.map(Option::unwrap_or_default))
+ } else {
+ T::cast(value).map(Self::splat).map_err(|msg| {
+ with_alternative(
+ msg,
+ "dictionary with any of \
+ `left`, `top`, `right`, `bottom`, \
+ `x`, `y`, or `rest` as keys",
+ )
+ })
+ }
+ }
+}
+
+impl<T> Cast for Corners<T>
+where
+ T: Cast + Default + 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).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),
+ };
+
+ if let Some((key, _)) = dict.iter().next() {
+ return Err(format!("unexpected key {key:?}"));
+ }
+
+ Ok(corners.map(Option::unwrap_or_default))
+ } else {
+ T::cast(value).map(Self::splat).map_err(|msg| {
+ with_alternative(
+ msg,
+ "dictionary with any of \
+ `top-left`, `top-right`, `bottom-right`, `bottom-left`, \
+ `left`, `top`, `right`, `bottom`, or `rest` as keys",
+ )
+ })
+ }
+ }
+}
diff --git a/src/model/content.rs b/src/model/content.rs
index 7828a3cd..5f0536c3 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -21,21 +21,6 @@ use crate::library::text::{
use crate::util::EcoString;
use crate::World;
-/// Layout content into a collection of pages.
-///
-/// Relayouts until all pinned locations are converged.
-#[comemo::memoize]
-pub fn layout(world: Tracked<dyn World>, content: &Content) -> SourceResult<Vec<Frame>> {
- let styles = StyleChain::with_root(&world.config().styles);
- let scratch = Scratch::default();
-
- let mut builder = Builder::new(world, &scratch, true);
- builder.accept(content, styles)?;
-
- let (doc, shared) = builder.into_doc(styles)?;
- doc.layout(world, shared)
-}
-
/// Composable representation of styled content.
///
/// This results from:
@@ -332,7 +317,7 @@ impl Sum for Content {
}
/// Builds a document or a flow node from content.
-struct Builder<'a> {
+pub(super) struct Builder<'a> {
/// The core context.
world: Tracked<'a, dyn World>,
/// Scratch arenas for building.
@@ -349,7 +334,7 @@ struct Builder<'a> {
/// Temporary storage arenas for building.
#[derive(Default)]
-struct Scratch<'a> {
+pub(super) struct Scratch<'a> {
/// An arena where intermediate style chains are stored.
styles: Arena<StyleChain<'a>>,
/// An arena where intermediate content resulting from show rules is stored.
@@ -357,7 +342,11 @@ struct Scratch<'a> {
}
impl<'a> Builder<'a> {
- fn new(world: Tracked<'a, dyn World>, scratch: &'a Scratch<'a>, top: bool) -> Self {
+ pub fn new(
+ world: Tracked<'a, dyn World>,
+ scratch: &'a Scratch<'a>,
+ top: bool,
+ ) -> Self {
Self {
world,
scratch,
@@ -368,7 +357,7 @@ impl<'a> Builder<'a> {
}
}
- fn into_doc(
+ pub fn into_doc(
mut self,
styles: StyleChain<'a>,
) -> SourceResult<(DocNode, StyleChain<'a>)> {
@@ -377,7 +366,7 @@ impl<'a> Builder<'a> {
Ok((DocNode(pages), shared))
}
- fn into_flow(
+ pub fn into_flow(
mut self,
styles: StyleChain<'a>,
) -> SourceResult<(FlowNode, StyleChain<'a>)> {
@@ -386,7 +375,7 @@ impl<'a> Builder<'a> {
Ok((FlowNode(children), shared))
}
- fn accept(
+ pub fn accept(
&mut self,
content: &'a Content,
styles: StyleChain<'a>,
diff --git a/src/model/dict.rs b/src/model/dict.rs
new file mode 100644
index 00000000..3e4fd956
--- /dev/null
+++ b/src/model/dict.rs
@@ -0,0 +1,193 @@
+use std::collections::BTreeMap;
+use std::fmt::{self, Debug, Formatter, Write};
+use std::ops::{Add, AddAssign};
+use std::sync::Arc;
+
+use super::{Args, Array, Func, Str, Value, Vm};
+use crate::diag::{SourceResult, StrResult};
+use crate::syntax::is_ident;
+use crate::syntax::Spanned;
+use crate::util::ArcExt;
+
+/// Create a new [`Dict`] from key-value pairs.
+#[allow(unused_macros)]
+macro_rules! dict {
+ ($($key:expr => $value:expr),* $(,)?) => {{
+ #[allow(unused_mut)]
+ let mut map = std::collections::BTreeMap::new();
+ $(map.insert($key.into(), $value.into());)*
+ $crate::model::Dict::from_map(map)
+ }};
+}
+
+/// 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 get(&self, key: &str) -> StrResult<&Value> {
+ self.0.get(key).ok_or_else(|| missing_key(key))
+ }
+
+ /// Mutably borrow the value the given `key` maps to.
+ ///
+ /// This inserts the key with [`None`](Value::None) as the value if not
+ /// present so far.
+ pub fn get_mut(&mut self, key: Str) -> &mut Value {
+ Arc::make_mut(&mut self.0).entry(key).or_default()
+ }
+
+ /// 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`.
+ pub fn remove(&mut self, key: &str) -> StrResult<()> {
+ match Arc::make_mut(&mut self.0).remove(key) {
+ Some(_) => Ok(()),
+ None => Err(missing_key(key)),
+ }
+ }
+
+ /// Remove the value if the dictionary contains the given key.
+ pub fn take(&mut self, key: &str) -> Option<Value> {
+ Arc::make_mut(&mut self.0).remove(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()
+ }
+
+ /// Transform each pair in the array with a function.
+ pub fn map(&self, vm: &mut Vm, f: Spanned<Func>) -> SourceResult<Array> {
+ self.iter()
+ .map(|(key, value)| {
+ let args = Args::new(f.span, [Value::Str(key.clone()), value.clone()]);
+ f.v.call(vm, args)
+ })
+ .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()
+ }
+}
+
+/// The missing key access error message.
+#[cold]
+fn missing_key(key: &str) -> String {
+ 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/model/eval.rs b/src/model/eval.rs
new file mode 100644
index 00000000..aa5f0378
--- /dev/null
+++ b/src/model/eval.rs
@@ -0,0 +1,1212 @@
+//! Evaluation of markup into modules.
+
+use std::collections::BTreeMap;
+use std::sync::Arc;
+
+use comemo::{Track, Tracked};
+use unicode_segmentation::UnicodeSegmentation;
+
+use super::{
+ methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func,
+ Pattern, Recipe, Scope, Scopes, StyleEntry, StyleMap, Value, Vm,
+};
+use crate::diag::{At, SourceResult, StrResult, Trace, Tracepoint};
+use crate::geom::{Angle, Em, Fraction, Length, Ratio};
+use crate::library;
+use crate::syntax::ast::TypedNode;
+use crate::syntax::{ast, SourceId, Span, Spanned, Unit};
+use crate::util::EcoString;
+use crate::World;
+
+/// Evaluate a source file and return the resulting module.
+///
+/// Returns either a module containing a scope with top-level bindings and
+/// layoutable contents or diagnostics in the form of a vector of error
+/// messages with file and span information.
+#[comemo::memoize]
+pub fn eval(
+ world: Tracked<dyn World>,
+ route: Tracked<Route>,
+ id: SourceId,
+) -> SourceResult<Module> {
+ // Prevent cyclic evaluation.
+ if route.contains(id) {
+ let path = world.source(id).path().display();
+ panic!("Tried to cyclicly evaluate {}", path);
+ }
+
+ // Evaluate the module.
+ let route = unsafe { Route::insert(route, id) };
+ let ast = world.source(id).ast()?;
+ let std = &world.config().std;
+ let scopes = Scopes::new(Some(std));
+ let mut vm = Vm::new(world, route.track(), Some(id), scopes);
+ let result = ast.eval(&mut vm);
+
+ // Handle control flow.
+ if let Some(flow) = vm.flow {
+ bail!(flow.forbidden());
+ }
+
+ // Assemble the module.
+ Ok(Module { scope: vm.scopes.top, content: result? })
+}
+
+/// A route of source ids.
+#[derive(Default)]
+pub struct Route {
+ parent: Option<Tracked<'static, Self>>,
+ id: Option<SourceId>,
+}
+
+impl Route {
+ /// Create a new, empty route.
+ pub fn new(id: Option<SourceId>) -> Self {
+ Self { 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))
+ }
+}
+
+/// An evaluated module, ready for importing or layouting.
+#[derive(Debug, Clone)]
+pub struct Module {
+ /// The top-level definitions that were bound in this module.
+ pub scope: Scope,
+ /// The module's layoutable contents.
+ pub content: Content,
+}
+
+/// Evaluate an expression.
+pub 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.children())
+ }
+}
+
+/// Evaluate a stream of markup nodes.
+fn eval_markup(
+ vm: &mut Vm,
+ nodes: &mut impl Iterator<Item = ast::MarkupNode>,
+) -> SourceResult<Content> {
+ let flow = vm.flow.take();
+ let mut seq = Vec::with_capacity(nodes.size_hint().1.unwrap_or_default());
+
+ while let Some(node) = nodes.next() {
+ seq.push(match node {
+ ast::MarkupNode::Expr(ast::Expr::Set(set)) => {
+ let styles = set.eval(vm)?;
+ if vm.flow.is_some() {
+ break;
+ }
+
+ eval_markup(vm, nodes)?.styled_with_map(styles)
+ }
+ ast::MarkupNode::Expr(ast::Expr::Show(show)) => {
+ let recipe = show.eval(vm)?;
+ if vm.flow.is_some() {
+ break;
+ }
+
+ eval_markup(vm, nodes)?
+ .styled_with_entry(StyleEntry::Recipe(recipe).into())
+ }
+ ast::MarkupNode::Expr(ast::Expr::Wrap(wrap)) => {
+ let tail = eval_markup(vm, nodes)?;
+ vm.scopes.top.define(wrap.binding().take(), tail);
+ wrap.body().eval(vm)?.display()
+ }
+
+ _ => node.eval(vm)?,
+ });
+
+ if vm.flow.is_some() {
+ break;
+ }
+ }
+
+ if flow.is_some() {
+ vm.flow = flow;
+ }
+
+ Ok(Content::sequence(seq))
+}
+
+impl Eval for ast::MarkupNode {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ match self {
+ Self::Space(v) => v.eval(vm),
+ Self::Linebreak(v) => v.eval(vm),
+ Self::Text(v) => v.eval(vm),
+ Self::Escape(v) => v.eval(vm),
+ Self::Shorthand(v) => v.eval(vm),
+ Self::SmartQuote(v) => v.eval(vm),
+ Self::Strong(v) => v.eval(vm),
+ Self::Emph(v) => v.eval(vm),
+ Self::Link(v) => v.eval(vm),
+ Self::Raw(v) => v.eval(vm),
+ Self::Math(v) => v.eval(vm),
+ Self::Heading(v) => v.eval(vm),
+ Self::List(v) => v.eval(vm),
+ Self::Enum(v) => v.eval(vm),
+ Self::Desc(v) => v.eval(vm),
+ Self::Label(v) => v.eval(vm),
+ Self::Ref(v) => v.eval(vm),
+ Self::Expr(v) => v.eval(vm).map(Value::display),
+ }
+ }
+}
+
+impl Eval for ast::Space {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(if self.newlines() < 2 {
+ Content::Space
+ } else {
+ Content::Parbreak
+ })
+ }
+}
+
+impl Eval for ast::Linebreak {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::Linebreak { justify: false })
+ }
+}
+
+impl Eval for ast::Text {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::Text(self.get().clone()))
+ }
+}
+
+impl Eval for ast::Escape {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::Text(self.get().into()))
+ }
+}
+
+impl Eval for ast::Shorthand {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::Text(self.get().into()))
+ }
+}
+
+impl Eval for ast::SmartQuote {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::Quote { double: self.double() })
+ }
+}
+
+impl Eval for ast::Strong {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::show(library::text::StrongNode(
+ self.body().eval(vm)?,
+ )))
+ }
+}
+
+impl Eval for ast::Emph {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::show(library::text::EmphNode(
+ self.body().eval(vm)?,
+ )))
+ }
+}
+
+impl Eval for ast::Link {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::show(library::text::LinkNode::from_url(
+ self.url().clone(),
+ )))
+ }
+}
+
+impl Eval for ast::Raw {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ let content = Content::show(library::text::RawNode {
+ text: self.text().clone(),
+ block: self.block(),
+ });
+ Ok(match self.lang() {
+ Some(_) => content.styled(library::text::RawNode::LANG, self.lang().cloned()),
+ None => content,
+ })
+ }
+}
+
+impl Eval for ast::Math {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let nodes = self
+ .children()
+ .map(|node| node.eval(vm))
+ .collect::<SourceResult<_>>()?;
+ Ok(Content::show(library::math::MathNode::Row(
+ Arc::new(nodes),
+ self.span(),
+ )))
+ }
+}
+
+impl Eval for ast::MathNode {
+ type Output = library::math::MathNode;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(match self {
+ Self::Space(_) => library::math::MathNode::Space,
+ Self::Linebreak(_) => library::math::MathNode::Linebreak,
+ Self::Escape(c) => library::math::MathNode::Atom(c.get().into()),
+ Self::Atom(atom) => library::math::MathNode::Atom(atom.get().clone()),
+ Self::Script(node) => node.eval(vm)?,
+ Self::Frac(node) => node.eval(vm)?,
+ Self::Align(node) => node.eval(vm)?,
+ Self::Group(node) => library::math::MathNode::Row(
+ Arc::new(
+ node.children()
+ .map(|node| node.eval(vm))
+ .collect::<SourceResult<_>>()?,
+ ),
+ node.span(),
+ ),
+ Self::Expr(expr) => match expr.eval(vm)?.display() {
+ Content::Text(text) => library::math::MathNode::Atom(text),
+ _ => bail!(expr.span(), "expected text"),
+ },
+ })
+ }
+}
+
+impl Eval for ast::Script {
+ type Output = library::math::MathNode;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(library::math::MathNode::Script(Arc::new(
+ library::math::ScriptNode {
+ base: self.base().eval(vm)?,
+ sub: self
+ .sub()
+ .map(|node| node.eval(vm))
+ .transpose()?
+ .map(|node| node.unparen()),
+ sup: self
+ .sup()
+ .map(|node| node.eval(vm))
+ .transpose()?
+ .map(|node| node.unparen()),
+ },
+ )))
+ }
+}
+
+impl Eval for ast::Frac {
+ type Output = library::math::MathNode;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(library::math::MathNode::Frac(Arc::new(
+ library::math::FracNode {
+ num: self.num().eval(vm)?.unparen(),
+ denom: self.denom().eval(vm)?.unparen(),
+ },
+ )))
+ }
+}
+
+impl Eval for ast::Align {
+ type Output = library::math::MathNode;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(library::math::MathNode::Align(self.count()))
+ }
+}
+
+impl Eval for ast::Heading {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::show(library::structure::HeadingNode {
+ body: self.body().eval(vm)?,
+ level: self.level(),
+ }))
+ }
+}
+
+impl Eval for ast::ListItem {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let body = Box::new(self.body().eval(vm)?);
+ Ok(Content::Item(library::structure::ListItem::List(body)))
+ }
+}
+
+impl Eval for ast::EnumItem {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let number = self.number();
+ let body = Box::new(self.body().eval(vm)?);
+ Ok(Content::Item(library::structure::ListItem::Enum(
+ number, body,
+ )))
+ }
+}
+
+impl Eval for ast::DescItem {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let term = self.term().eval(vm)?;
+ let body = self.body().eval(vm)?;
+ Ok(Content::Item(library::structure::ListItem::Desc(Box::new(
+ library::structure::DescItem { term, body },
+ ))))
+ }
+}
+
+impl Eval for ast::Label {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::Empty)
+ }
+}
+
+impl Eval for ast::Ref {
+ type Output = Content;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(Content::show(library::structure::RefNode(
+ self.get().clone(),
+ )))
+ }
+}
+
+impl Eval for ast::Expr {
+ type Output = Value;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let forbidden = |name| {
+ error!(
+ self.span(),
+ "{} is only allowed directly in code and content blocks", name
+ )
+ };
+
+ match self {
+ Self::Lit(v) => v.eval(vm),
+ Self::Ident(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::MethodCall(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::Wrap(_) => bail!(forbidden("wrap")),
+ 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),
+ }
+ }
+}
+
+impl Eval for ast::Lit {
+ type Output = Value;
+
+ fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
+ Ok(match self.kind() {
+ ast::LitKind::None => Value::None,
+ ast::LitKind::Auto => Value::Auto,
+ ast::LitKind::Bool(v) => Value::Bool(v),
+ ast::LitKind::Int(v) => Value::Int(v),
+ ast::LitKind::Float(v) => Value::Float(v),
+ ast::LitKind::Numeric(v, unit) => match unit {
+ Unit::Length(unit) => Length::with_unit(v, unit).into(),
+ Unit::Angle(unit) => Angle::with_unit(v, unit).into(),
+ Unit::Em => Em::new(v).into(),
+ Unit::Fr => Fraction::new(v).into(),
+ Unit::Percent => Ratio::new(v / 100.0).into(),
+ },
+ ast::LitKind::Str(v) => Value::Str(v.into()),
+ })
+ }
+}
+
+impl Eval for ast::Ident {
+ type Output = Value;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ vm.scopes.get(self).cloned().at(self.span())
+ }
+}
+
+impl Eval for ast::CodeBlock {
+ type Output = Value;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ vm.scopes.enter();
+ let output = eval_code(vm, &mut self.exprs())?;
+ vm.scopes.exit();
+ Ok(output)
+ }
+}
+
+/// 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)?;
+ let entry = StyleEntry::Recipe(recipe).into();
+ if vm.flow.is_some() {
+ break;
+ }
+
+ let tail = eval_code(vm, exprs)?.display();
+ Value::Content(tail.styled_with_entry(entry))
+ }
+ ast::Expr::Wrap(wrap) => {
+ let tail = eval_code(vm, exprs)?;
+ vm.scopes.top.define(wrap.binding().take(), tail);
+ wrap.body().eval(vm)?
+ }
+
+ _ => 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 = Vec::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().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),
+ };
+ Ok(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)?;
+ Ok(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 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 object = self.target().eval(vm)?;
+ let span = self.field().span();
+ let field = self.field().take();
+
+ Ok(match object {
+ Value::Dict(dict) => dict.get(&field).at(span)?.clone(),
+
+ Value::Content(Content::Show(_, Some(dict))) => dict
+ .get(&field)
+ .map_err(|_| format!("unknown field {field:?}"))
+ .at(span)?
+ .clone(),
+
+ v => bail!(
+ self.target().span(),
+ "cannot access field on {}",
+ v.type_name()
+ ),
+ })
+ }
+}
+
+impl Eval for ast::FuncCall {
+ type Output = Value;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let callee = self.callee().eval(vm)?;
+ let args = self.args().eval(vm)?;
+
+ Ok(match callee {
+ Value::Array(array) => array.get(args.into_index()?).at(self.span())?.clone(),
+ Value::Dict(dict) => dict.get(&args.into_key()?).at(self.span())?.clone(),
+ Value::Func(func) => {
+ let point = || Tracepoint::Call(func.name().map(Into::into));
+ func.call(vm, args).trace(vm.world, point, self.span())?
+ }
+
+ v => bail!(
+ self.callee().span(),
+ "expected callable or collection, found {}",
+ v.type_name(),
+ ),
+ })
+ }
+}
+
+impl Eval for ast::MethodCall {
+ type Output = Value;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let span = self.span();
+ let method = self.method().take();
+ let point = || Tracepoint::Call(Some(method.clone()));
+
+ Ok(if methods::is_mutating(&method) {
+ let args = self.args().eval(vm)?;
+ let mut value = self.target().access(vm)?;
+ methods::call_mut(&mut value, &method, args, span)
+ .trace(vm.world, point, span)?;
+ Value::None
+ } else {
+ let value = self.target().eval(vm)?;
+ let args = self.args().eval(vm)?;
+ methods::call(vm, value, &method, args, span).trace(vm.world, point, span)?
+ })
+ }
+}
+
+impl Eval for ast::Args {
+ type Output = Args;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let mut items = Vec::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 actual function.
+ Ok(Value::Func(Func::from_closure(Closure {
+ location: vm.location,
+ name,
+ captured,
+ params,
+ sink,
+ body: self.body(),
+ })))
+ }
+}
+
+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.scopes.top.define(self.binding().take(), value);
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::SetRule {
+ type Output = StyleMap;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let target = self.target();
+ let target = target.eval(vm)?.cast::<Func>().at(target.span())?;
+ let args = self.args().eval(vm)?;
+ Ok(target.set(args)?)
+ }
+}
+
+impl Eval for ast::ShowRule {
+ type Output = Recipe;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ // Evaluate the target function.
+ let pattern = self.pattern();
+ let pattern = pattern.eval(vm)?.cast::<Pattern>().at(pattern.span())?;
+
+ // Collect captured variables.
+ let captured = {
+ let mut visitor = CapturesVisitor::new(&vm.scopes);
+ visitor.visit(self.as_untyped());
+ visitor.finish()
+ };
+
+ // Define parameters.
+ let mut params = vec![];
+ if let Some(binding) = self.binding() {
+ params.push((binding.take(), None));
+ }
+
+ // Define the recipe function.
+ let body = self.body();
+ let span = body.span();
+ let func = Func::from_closure(Closure {
+ location: vm.location,
+ name: None,
+ captured,
+ params,
+ sink: None,
+ body,
+ });
+
+ Ok(Recipe { pattern, func: Spanned::new(func, span) })
+ }
+}
+
+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 condition = self.condition();
+ while condition.eval(vm)?.cast::<bool>().at(condition.span())? {
+ 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 => {}
+ }
+ }
+
+ if flow.is_some() {
+ vm.flow = flow;
+ }
+
+ Ok(output)
+ }
+}
+
+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;
+ vm.scopes.enter();
+
+ macro_rules! iter {
+ (for ($($binding:ident => $value:ident),*) in $iter:expr) => {{
+ #[allow(unused_parens)]
+ for ($($value),*) in $iter {
+ $(vm.scopes.top.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 => {}
+ }
+ }
+
+ }};
+ }
+
+ let iter = self.iter().eval(vm)?;
+ let pattern = self.pattern();
+ let key = pattern.key().map(ast::Ident::take);
+ let value = pattern.value().take();
+
+ 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;
+ }
+
+ vm.scopes.exit();
+ Ok(output)
+ }
+}
+
+impl Eval for ast::ModuleImport {
+ type Output = Value;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let span = self.path().span();
+ let path = self.path().eval(vm)?.cast::<EcoString>().at(span)?;
+ let module = import(vm, &path, span)?;
+
+ match self.imports() {
+ ast::Imports::Wildcard => {
+ for (var, value) in module.scope.iter() {
+ vm.scopes.top.define(var, value.clone());
+ }
+ }
+ ast::Imports::Items(idents) => {
+ for ident in idents {
+ if let Some(value) = module.scope.get(&ident) {
+ vm.scopes.top.define(ident.take(), value.clone());
+ } else {
+ bail!(ident.span(), "unresolved import");
+ }
+ }
+ }
+ }
+
+ Ok(Value::None)
+ }
+}
+
+impl Eval for ast::ModuleInclude {
+ type Output = Content;
+
+ fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
+ let span = self.path().span();
+ let path = self.path().eval(vm)?.cast::<EcoString>().at(span)?;
+ let module = import(vm, &path, span)?;
+ Ok(module.content.clone())
+ }
+}
+
+/// Process an import of a module relative to the current location.
+fn import(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
+ // 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 module =
+ eval(vm.world, vm.route, id).trace(vm.world, || Tracepoint::Import, span)?;
+
+ Ok(module)
+}
+
+impl Eval for ast::BreakStmt {
+ 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::ContinueStmt {
+ 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::ReturnStmt {
+ 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.
+pub 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::FieldAccess(v) => v.access(vm),
+ Self::FuncCall(v) => v.access(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> {
+ vm.scopes.get_mut(self).at(self.span())
+ }
+}
+
+impl Access for ast::FieldAccess {
+ fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ Ok(match self.target().access(vm)? {
+ Value::Dict(dict) => dict.get_mut(self.field().take().into()),
+ v => bail!(
+ self.target().span(),
+ "expected dictionary, found {}",
+ v.type_name(),
+ ),
+ })
+ }
+}
+
+impl Access for ast::FuncCall {
+ fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
+ let args = self.args().eval(vm)?;
+ Ok(match self.callee().access(vm)? {
+ Value::Array(array) => array.get_mut(args.into_index()?).at(self.span())?,
+ Value::Dict(dict) => dict.get_mut(args.into_key()?),
+ v => bail!(
+ self.callee().span(),
+ "expected collection, found {}",
+ v.type_name(),
+ ),
+ })
+ }
+}
diff --git a/src/model/func.rs b/src/model/func.rs
new file mode 100644
index 00000000..a4f63aa1
--- /dev/null
+++ b/src/model/func.rs
@@ -0,0 +1,249 @@
+use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
+use std::sync::Arc;
+
+use comemo::{Track, Tracked};
+
+use super::{
+ Args, Content, Eval, Flow, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm,
+};
+use crate::diag::{SourceResult, StrResult};
+use crate::syntax::ast::Expr;
+use crate::syntax::SourceId;
+use crate::util::EcoString;
+use crate::World;
+
+/// An evaluatable function.
+#[derive(Clone, Hash)]
+pub struct Func(Arc<Repr>);
+
+/// 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 native rust function.
+ pub fn from_fn(
+ name: &'static str,
+ func: fn(&mut Vm, &mut Args) -> SourceResult<Value>,
+ ) -> Self {
+ Self(Arc::new(Repr::Native(Native {
+ name,
+ func,
+ set: None,
+ node: None,
+ })))
+ }
+
+ /// Create a new function from a native rust node.
+ pub fn from_node<T: Node>(name: &'static str) -> Self {
+ Self(Arc::new(Repr::Native(Native {
+ name,
+ 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: T::SHOWABLE.then(|| NodeId::of::<T>()),
+ })))
+ }
+
+ /// Create a new function from a closure.
+ pub fn from_closure(closure: Closure) -> Self {
+ Self(Arc::new(Repr::Closure(closure)))
+ }
+
+ /// Apply the given arguments to the function.
+ pub fn with(self, args: Args) -> Self {
+ Self(Arc::new(Repr::With(self, args)))
+ }
+
+ /// The name of the function.
+ pub fn name(&self) -> Option<&str> {
+ match self.0.as_ref() {
+ Repr::Native(native) => Some(native.name),
+ Repr::Closure(closure) => closure.name.as_deref(),
+ Repr::With(func, _) => func.name(),
+ }
+ }
+
+ /// The number of positional arguments this function takes, if known.
+ pub fn argc(&self) -> Option<usize> {
+ match self.0.as_ref() {
+ Repr::Closure(closure) => Some(
+ closure.params.iter().filter(|(_, default)| default.is_none()).count(),
+ ),
+ 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> {
+ let value = match self.0.as_ref() {
+ Repr::Native(native) => (native.func)(vm, &mut args)?,
+ Repr::Closure(closure) => closure.call(vm, &mut args)?,
+ Repr::With(wrapped, applied) => {
+ args.items.splice(.. 0, applied.items.iter().cloned());
+ return wrapped.call(vm, args);
+ }
+ };
+ args.finish()?;
+ Ok(value)
+ }
+
+ /// 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 mut vm = Vm::new(world, route.track(), None, Scopes::new(None));
+ self.call(&mut vm, args)
+ }
+
+ /// Execute the function's set rule and return the resulting style map.
+ pub fn set(&self, mut args: Args) -> SourceResult<StyleMap> {
+ let styles = match self.0.as_ref() {
+ Repr::Native(Native { set: Some(set), .. }) => set(&mut args)?,
+ _ => StyleMap::new(),
+ };
+ args.finish()?;
+ Ok(styles)
+ }
+
+ /// The id of the node to customize with this function's show rule.
+ pub fn node(&self) -> StrResult<NodeId> {
+ match self.0.as_ref() {
+ Repr::Native(Native { node: Some(id), .. }) => Ok(*id),
+ _ => Err("this function cannot be customized with show")?,
+ }
+ }
+}
+
+impl Debug for Func {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self.name() {
+ Some(name) => f.write_str(name),
+ None => f.write_str("(..) => {..}"),
+ }
+ }
+}
+
+impl PartialEq for Func {
+ fn eq(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.0, &other.0)
+ }
+}
+
+/// A function defined by a native rust function or node.
+struct Native {
+ /// The name of the function.
+ pub name: &'static str,
+ /// The function pointer.
+ pub func: fn(&mut Vm, &mut Args) -> SourceResult<Value>,
+ /// The set rule.
+ pub set: Option<fn(&mut Args) -> SourceResult<StyleMap>>,
+ /// The id of the node to customize with this function's show rule.
+ pub node: Option<NodeId>,
+}
+
+impl Hash for Native {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.name.hash(state);
+ (self.func as usize).hash(state);
+ self.set.map(|set| set as usize).hash(state);
+ self.node.hash(state);
+ }
+}
+
+/// A constructable, stylable content node.
+pub trait Node: 'static {
+ /// Whether this node can be customized through a show rule.
+ const SHOWABLE: bool;
+
+ /// Construct a node from the arguments.
+ ///
+ /// This is passed only the arguments that remain after execution of the
+ /// node's set rule.
+ fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>;
+
+ /// Parse relevant arguments into style properties for this node.
+ ///
+ /// When `constructor` is true, [`construct`](Self::construct) will run
+ /// after this invocation of `set` with the remaining arguments.
+ fn set(args: &mut Args, constructor: bool) -> SourceResult<StyleMap>;
+}
+
+/// A user-defined closure.
+#[derive(Hash)]
+pub struct Closure {
+ /// The source file where the closure was defined.
+ pub location: Option<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.
+ pub fn call(&self, vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ // 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 = self.captured.clone();
+
+ // Parse the arguments according to the parameter list.
+ for (param, default) in &self.params {
+ scopes.top.define(param.clone(), match default {
+ None => args.expect::<Value>(param)?,
+ Some(default) => {
+ args.named::<Value>(param)?.unwrap_or_else(|| default.clone())
+ }
+ });
+ }
+
+ // Put the remaining arguments into the sink.
+ if let Some(sink) = &self.sink {
+ scopes.top.define(sink.clone(), args.take());
+ }
+
+ // Determine the route inside the closure.
+ let detached = vm.location.is_none();
+ let fresh = Route::new(self.location);
+ let route = if detached { fresh.track() } else { vm.route };
+
+ // Evaluate the body.
+ let mut sub = Vm::new(vm.world, route, self.location, scopes);
+ let result = self.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
+ }
+}
diff --git a/src/model/layout.rs b/src/model/layout.rs
index 8064afff..09888ba5 100644
--- a/src/model/layout.rs
+++ b/src/model/layout.rs
@@ -8,8 +8,8 @@ use std::sync::Arc;
use comemo::{Prehashed, Tracked};
use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry};
+use super::{Builder, Content, RawAlign, RawLength, Scratch};
use crate::diag::SourceResult;
-use crate::eval::{RawAlign, RawLength};
use crate::frame::{Element, Frame};
use crate::geom::{
Align, Geometry, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke,
@@ -18,6 +18,21 @@ use crate::library::graphics::MoveNode;
use crate::library::layout::{AlignNode, PadNode};
use crate::World;
+/// Layout content into a collection of pages.
+///
+/// Relayouts until all pinned locations are converged.
+#[comemo::memoize]
+pub fn layout(world: Tracked<dyn World>, content: &Content) -> SourceResult<Vec<Frame>> {
+ let styles = StyleChain::with_root(&world.config().styles);
+ let scratch = Scratch::default();
+
+ let mut builder = Builder::new(world, &scratch, true);
+ builder.accept(content, styles)?;
+
+ let (doc, shared) = builder.into_doc(styles)?;
+ doc.layout(world, shared)
+}
+
/// A node that can be layouted into a sequence of regions.
///
/// Layouting returns one frame per used region.
diff --git a/src/model/methods.rs b/src/model/methods.rs
new file mode 100644
index 00000000..57fff681
--- /dev/null
+++ b/src/model/methods.rs
@@ -0,0 +1,167 @@
+//! Methods on values.
+
+use super::{Args, Value, Vm};
+use crate::diag::{At, SourceResult};
+use crate::syntax::Span;
+use crate::util::EcoString;
+
+/// 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::Str(string) => match method {
+ "len" => Value::Int(string.len() as i64),
+ "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)?)
+ }
+ "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().cloned().unwrap_or(Value::None),
+ "last" => array.last().cloned().unwrap_or(Value::None),
+ "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")?)?),
+ "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()),
+ "keys" => Value::Array(dict.keys()),
+ "values" => Value::Array(dict.values()),
+ "pairs" => Value::Array(dict.map(vm, args.expect("function")?)?),
+ _ => return missing(),
+ },
+
+ Value::Func(func) => match method {
+ "with" => Value::Func(func.clone().with(args.take())),
+ _ => return missing(),
+ },
+
+ Value::Args(args) => match method {
+ "positional" => Value::Array(args.to_positional()),
+ "named" => Value::Dict(args.to_named()),
+ _ => return missing(),
+ },
+
+ 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(),
+ },
+
+ _ => 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<()> {
+ let name = value.type_name();
+ let missing = || Err(missing_method(name, method)).at(span);
+
+ match value {
+ Value::Array(array) => match method {
+ "push" => array.push(args.expect("value")?),
+ "pop" => array.pop().at(span)?,
+ "insert" => {
+ array.insert(args.expect("index")?, args.expect("value")?).at(span)?
+ }
+ "remove" => array.remove(args.expect("index")?).at(span)?,
+ _ => return missing(),
+ },
+
+ Value::Dict(dict) => match method {
+ "remove" => dict.remove(&args.expect::<EcoString>("key")?).at(span)?,
+ _ => return missing(),
+ },
+
+ _ => return missing(),
+ }
+
+ args.finish()?;
+ Ok(())
+}
+
+/// Whether a specific method is mutating.
+pub fn is_mutating(method: &str) -> bool {
+ matches!(method, "push" | "pop" | "insert" | "remove")
+}
+
+/// The missing method error message.
+#[cold]
+fn missing_method(type_name: &str, method: &str) -> String {
+ format!("type {type_name} has no method `{method}`")
+}
diff --git a/src/model/mod.rs b/src/model/mod.rs
index 5c8b82c0..0ea5cbdd 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -1,18 +1,50 @@
-//! Styled and structured representation of layoutable content.
+//! Layout and computation model.
#[macro_use]
mod styles;
mod collapse;
mod content;
+mod eval;
mod layout;
mod property;
mod recipe;
mod show;
+#[macro_use]
+mod cast;
+#[macro_use]
+mod array;
+#[macro_use]
+mod dict;
+#[macro_use]
+mod str;
+#[macro_use]
+mod value;
+mod args;
+mod capture;
+mod func;
+pub mod methods;
+pub mod ops;
+mod raw;
+mod scope;
+mod vm;
+pub use self::str::*;
+pub use args::*;
+pub use array::*;
+pub use capture::*;
+pub use cast::*;
pub use collapse::*;
pub use content::*;
+pub use dict::*;
+pub use eval::*;
+pub use func::*;
pub use layout::*;
pub use property::*;
+pub use raw::*;
pub use recipe::*;
+pub use scope::*;
pub use show::*;
pub use styles::*;
+pub use typst_macros::node;
+pub use value::*;
+pub use vm::*;
diff --git a/src/model/ops.rs b/src/model/ops.rs
new file mode 100644
index 00000000..c521f704
--- /dev/null
+++ b/src/model/ops.rs
@@ -0,0 +1,398 @@
+//! Operations on values.
+
+use std::cmp::Ordering;
+
+use super::{RawAlign, RawLength, RawStroke, Regex, Smart, Value};
+use crate::diag::StrResult;
+use crate::geom::{Numeric, Relative, Spec, SpecAxis};
+use Value::*;
+
+/// Bail with a type mismatch error.
+macro_rules! mismatch {
+ ($fmt:expr, $($value:expr),* $(,)?) => {
+ return Err(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,
+ (Str(a), Str(b)) => Str(a + b),
+ (Str(a), Content(b)) => Content(super::Content::Text(a.into()) + b),
+ (Content(a), Str(b)) => Content(a + super::Content::Text(b.into())),
+ (Content(a), Content(b)) => Content(a + 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),
+
+ (Str(a), Str(b)) => Str(a + b),
+ (Content(a), Content(b)) => Content(a + b),
+ (Content(a), Str(b)) => Content(a + super::Content::Text(b.into())),
+ (Str(a), Content(b)) => Content(super::Content::Text(a.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(RawStroke {
+ 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::<RawAlign>(), b.downcast::<RawAlign>())
+ {
+ if a.axis() != b.axis() {
+ Value::dynamic(match a.axis() {
+ SpecAxis::Horizontal => Spec { x: a, y: b },
+ SpecAxis::Vertical => Spec { x: b, y: a },
+ })
+ } else {
+ return Err(format!("cannot add two {:?} alignments", a.axis()));
+ }
+ } else {
+ 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> {
+ 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(div_length(a, b)?),
+ (Length(a), Relative(b)) if b.rel.is_zero() => Float(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(div_length(a.abs, b)?),
+ (Relative(a), Ratio(b)) if a.abs.is_zero() => Float(a.rel / b),
+ (Relative(a), Relative(b)) => Float(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),
+ })
+}
+
+/// Try to divide two lengths.
+fn div_length(a: RawLength, b: RawLength) -> StrResult<f64> {
+ if a.length.is_zero() && b.length.is_zero() {
+ Ok(a.em / b.em)
+ } else if a.em.is_zero() && b.em.is_zero() {
+ Ok(a.length / b.length)
+ } else {
+ return Err("cannot divide these two lengths".into());
+ }
+}
+
+/// Try to divide two relative lengths.
+fn div_relative(a: Relative<RawLength>, b: Relative<RawLength>) -> StrResult<f64> {
+ if a.rel.is_zero() && b.rel.is_zero() {
+ div_length(a.abs, b.abs)
+ } else if a.abs.is_zero() && b.abs.is_zero() {
+ Ok(a.rel / b.rel)
+ } else {
+ return Err("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,
+ (Str(a), Str(b)) => a == b,
+ (Content(a), Content(b)) => a == b,
+ (Array(a), Array(b)) => a == b,
+ (Dict(a), Dict(b)) => a == b,
+ (Func(a), Func(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> {
+ Some(match (lhs, rhs) {
+ (Str(a), Str(b)) => b.as_str().contains(a.as_str()),
+ (Dyn(a), Str(b)) => {
+ if let Some(regex) = a.downcast::<Regex>() {
+ regex.is_match(b)
+ } else {
+ return Option::None;
+ }
+ }
+ (Str(a), Dict(b)) => b.contains(a),
+ (a, Array(b)) => b.contains(a),
+ _ => return Option::None,
+ })
+}
diff --git a/src/model/property.rs b/src/model/property.rs
index ab4f02e3..ed2ab1d0 100644
--- a/src/model/property.rs
+++ b/src/model/property.rs
@@ -5,8 +5,7 @@ use std::sync::Arc;
use comemo::Prehashed;
-use super::{Interruption, NodeId, StyleChain};
-use crate::eval::{RawLength, Smart};
+use super::{Interruption, NodeId, RawLength, Smart, StyleChain};
use crate::geom::{Corners, Length, Numeric, Relative, Sides, Spec};
use crate::library::layout::PageNode;
use crate::library::structure::{DescNode, EnumNode, ListNode};
diff --git a/src/model/raw.rs b/src/model/raw.rs
new file mode 100644
index 00000000..b40a88ec
--- /dev/null
+++ b/src/model/raw.rs
@@ -0,0 +1,293 @@
+use std::cmp::Ordering;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::{Add, Div, Mul, Neg};
+
+use super::{Fold, Resolve, Smart, StyleChain, Value};
+use crate::geom::{
+ Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke,
+};
+use crate::library::text::TextNode;
+
+/// The unresolved alignment representation.
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum RawAlign {
+ /// Align at the start side of the text direction.
+ Start,
+ /// Align at the end side of the text direction.
+ End,
+ /// Align at a specific alignment.
+ Specific(Align),
+}
+
+impl Resolve for RawAlign {
+ type Output = Align;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ let dir = styles.get(TextNode::DIR);
+ match self {
+ Self::Start => dir.start().into(),
+ Self::End => dir.end().into(),
+ Self::Specific(align) => align,
+ }
+ }
+}
+
+impl RawAlign {
+ /// The axis this alignment belongs to.
+ pub const fn axis(self) -> SpecAxis {
+ match self {
+ Self::Start | Self::End => SpecAxis::Horizontal,
+ Self::Specific(align) => align.axis(),
+ }
+ }
+}
+
+impl From<Align> for RawAlign {
+ fn from(align: Align) -> Self {
+ Self::Specific(align)
+ }
+}
+
+impl Debug for RawAlign {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Start => f.pad("left"),
+ Self::End => f.pad("center"),
+ Self::Specific(align) => align.fmt(f),
+ }
+ }
+}
+
+dynamic! {
+ RawAlign: "alignment",
+}
+
+dynamic! {
+ Spec<RawAlign>: "2d alignment",
+}
+
+castable! {
+ Spec<Option<RawAlign>>,
+ Expected: "1d or 2d alignment",
+ @align: RawAlign => {
+ let mut aligns = Spec::default();
+ aligns.set(align.axis(), Some(*align));
+ aligns
+ },
+ @aligns: Spec<RawAlign> => aligns.map(Some),
+}
+
+/// The unresolved stroke representation.
+///
+/// In this representation, both fields are optional so that you can pass either
+/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
+/// this is expected.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct RawStroke<T = RawLength> {
+ /// The stroke's paint.
+ pub paint: Smart<Paint>,
+ /// The stroke's thickness.
+ pub thickness: Smart<T>,
+}
+
+impl RawStroke<Length> {
+ /// Unpack the stroke, filling missing fields from the `default`.
+ pub fn unwrap_or(self, default: Stroke) -> Stroke {
+ Stroke {
+ paint: self.paint.unwrap_or(default.paint),
+ thickness: self.thickness.unwrap_or(default.thickness),
+ }
+ }
+
+ /// Unpack the stroke, filling missing fields with the default values.
+ pub fn unwrap_or_default(self) -> Stroke {
+ self.unwrap_or(Stroke::default())
+ }
+}
+
+impl Resolve for RawStroke {
+ type Output = RawStroke<Length>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ RawStroke {
+ paint: self.paint,
+ thickness: self.thickness.resolve(styles),
+ }
+ }
+}
+
+impl Fold for RawStroke<Length> {
+ type Output = Self;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ Self {
+ paint: self.paint.or(outer.paint),
+ thickness: self.thickness.or(outer.thickness),
+ }
+ }
+}
+
+impl<T: Debug> Debug for RawStroke<T> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match (self.paint, &self.thickness) {
+ (Smart::Custom(paint), Smart::Custom(thickness)) => {
+ write!(f, "{thickness:?} + {paint:?}")
+ }
+ (Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
+ (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
+ (Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
+ }
+ }
+}
+
+dynamic! {
+ RawStroke: "stroke",
+ Value::Length(thickness) => Self {
+ paint: Smart::Auto,
+ thickness: Smart::Custom(thickness),
+ },
+ Value::Color(color) => Self {
+ paint: Smart::Custom(color.into()),
+ thickness: Smart::Auto,
+ },
+}
+
+/// The unresolved length representation.
+///
+/// Currently supports absolute and em units, but support could quite easily be
+/// extended to other units that can be resolved through a style chain.
+/// Probably, it would be a good idea to then move to an enum representation
+/// that has a small footprint and allocates for the rare case that units are
+/// mixed.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct RawLength {
+ /// The absolute part.
+ pub length: Length,
+ /// The font-relative part.
+ pub em: Em,
+}
+
+impl RawLength {
+ /// The zero length.
+ pub const fn zero() -> Self {
+ Self { length: Length::zero(), em: Em::zero() }
+ }
+}
+
+impl Debug for RawLength {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match (self.length.is_zero(), self.em.is_zero()) {
+ (false, false) => write!(f, "{:?} + {:?}", self.length, self.em),
+ (true, false) => self.em.fmt(f),
+ (_, true) => self.length.fmt(f),
+ }
+ }
+}
+
+impl Resolve for Em {
+ type Output = Length;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ if self.is_zero() {
+ Length::zero()
+ } else {
+ self.at(styles.get(TextNode::SIZE))
+ }
+ }
+}
+
+impl Resolve for RawLength {
+ type Output = Length;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.length + self.em.resolve(styles)
+ }
+}
+
+impl Numeric for RawLength {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.length.is_finite() && self.em.is_finite()
+ }
+}
+
+impl PartialOrd for RawLength {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ if self.em.is_zero() && other.em.is_zero() {
+ self.length.partial_cmp(&other.length)
+ } else if self.length.is_zero() && other.length.is_zero() {
+ self.em.partial_cmp(&other.em)
+ } else {
+ None
+ }
+ }
+}
+
+impl From<Length> for RawLength {
+ fn from(length: Length) -> Self {
+ Self { length, em: Em::zero() }
+ }
+}
+
+impl From<Em> for RawLength {
+ fn from(em: Em) -> Self {
+ Self { length: Length::zero(), em }
+ }
+}
+
+impl From<Length> for Relative<RawLength> {
+ fn from(length: Length) -> Self {
+ Relative::from(RawLength::from(length))
+ }
+}
+
+impl Neg for RawLength {
+ type Output = Self;
+
+ fn neg(self) -> Self::Output {
+ Self { length: -self.length, em: -self.em }
+ }
+}
+
+impl Add for RawLength {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self {
+ length: self.length + rhs.length,
+ em: self.em + rhs.em,
+ }
+ }
+}
+
+sub_impl!(RawLength - RawLength -> RawLength);
+
+impl Mul<f64> for RawLength {
+ type Output = Self;
+
+ fn mul(self, rhs: f64) -> Self::Output {
+ Self {
+ length: self.length * rhs,
+ em: self.em * rhs,
+ }
+ }
+}
+
+impl Div<f64> for RawLength {
+ type Output = Self;
+
+ fn div(self, rhs: f64) -> Self::Output {
+ Self {
+ length: self.length / rhs,
+ em: self.em / rhs,
+ }
+ }
+}
+
+assign_impl!(RawLength += RawLength);
+assign_impl!(RawLength -= RawLength);
+assign_impl!(RawLength *= f64);
+assign_impl!(RawLength /= f64);
diff --git a/src/model/recipe.rs b/src/model/recipe.rs
index 27b1be42..05ef07a6 100644
--- a/src/model/recipe.rs
+++ b/src/model/recipe.rs
@@ -2,9 +2,11 @@ use std::fmt::{self, Debug, Formatter};
use comemo::Tracked;
-use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntry};
+use super::{
+ Args, Content, Func, Interruption, NodeId, Regex, Show, ShowNode, StyleChain,
+ StyleEntry, Value,
+};
use crate::diag::SourceResult;
-use crate::eval::{Args, Func, Regex, Value};
use crate::library::structure::{DescNode, EnumNode, ListNode};
use crate::syntax::Spanned;
use crate::World;
diff --git a/src/model/scope.rs b/src/model/scope.rs
new file mode 100644
index 00000000..1ab7032c
--- /dev/null
+++ b/src/model/scope.rs
@@ -0,0 +1,161 @@
+use std::collections::BTreeMap;
+use std::fmt::{self, Debug, Formatter};
+use std::hash::Hash;
+
+use super::{Args, Func, Node, Value, Vm};
+use crate::diag::{SourceResult, StrResult};
+use crate::util::EcoString;
+
+/// 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 base scope.
+ pub base: Option<&'a Scope>,
+}
+
+impl<'a> Scopes<'a> {
+ /// Create a new, empty hierarchy of scopes.
+ pub fn new(base: Option<&'a Scope>) -> 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.into_iter())
+ .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(|| {
+ if self.base.map_or(false, |base| base.get(var).is_some()) {
+ "cannot mutate a constant"
+ } else {
+ "unknown variable"
+ }
+ })?
+ }
+}
+
+/// A map from binding names to values.
+#[derive(Default, Clone, Hash)]
+pub struct Scope(BTreeMap<EcoString, Slot>);
+
+impl Scope {
+ /// Create a new empty scope.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Bind a value to a name.
+ pub fn define(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) {
+ self.0.insert(name.into(), Slot::new(value.into(), Kind::Normal));
+ }
+
+ /// Define a function through a native rust function.
+ pub fn def_fn(
+ &mut self,
+ name: &'static str,
+ func: fn(&mut Vm, &mut Args) -> SourceResult<Value>,
+ ) {
+ self.define(name, Func::from_fn(name, func));
+ }
+
+ /// Define a function through a native rust node.
+ pub fn def_node<T: Node>(&mut self, name: &'static str) {
+ self.define(name, Func::from_node::<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 = (&str, &Value)> {
+ self.0.iter().map(|(k, v)| (k.as_str(), 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/model/show.rs b/src/model/show.rs
index b30b2264..bff69448 100644
--- a/src/model/show.rs
+++ b/src/model/show.rs
@@ -4,9 +4,8 @@ use std::sync::Arc;
use comemo::{Prehashed, Tracked};
-use super::{Content, NodeId, Selector, StyleChain};
+use super::{Content, Dict, NodeId, Selector, StyleChain};
use crate::diag::SourceResult;
-use crate::eval::Dict;
use crate::World;
/// A node that can be realized given some styles.
diff --git a/src/model/str.rs b/src/model/str.rs
new file mode 100644
index 00000000..62b37845
--- /dev/null
+++ b/src/model/str.rs
@@ -0,0 +1,470 @@
+use std::borrow::{Borrow, Cow};
+use std::fmt::{self, Debug, Formatter, Write};
+use std::hash::{Hash, Hasher};
+use std::ops::{Add, AddAssign, Deref};
+
+use unicode_segmentation::UnicodeSegmentation;
+
+use super::{Array, Dict, RawAlign, Value};
+use crate::diag::StrResult;
+use crate::util::EcoString;
+
+/// Create a new [`Str`] from a format string.
+#[allow(unused_macros)]
+macro_rules! format_str {
+ ($($tts:tt)*) => {{
+ $crate::model::Str::from(format_eco!($($tts)*))
+ }};
+}
+
+/// 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
+ }
+
+ /// The codepoints the string consists of.
+ pub fn codepoints(&self) -> Array {
+ self.as_str().chars().map(|c| Value::Str(c.into())).collect()
+ }
+
+ /// The grapheme clusters the string consists of.
+ pub fn graphemes(&self) -> Array {
+ self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect()
+ }
+
+ /// Extract a contigous substring.
+ 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.0[start .. end].into())
+ }
+
+ /// Resolve an index.
+ fn locate(&self, index: i64) -> Option<usize> {
+ usize::try_from(if index >= 0 {
+ index
+ } else {
+ self.len().checked_add(index)?
+ })
+ .ok()
+ }
+
+ /// 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)))
+ }
+}
+
+/// The out of bounds access error message.
+#[cold]
+fn out_of_bounds(index: i64, len: i64) -> String {
+ format!(
+ "string index out of bounds (index: {}, len: {})",
+ index, len
+ )
+}
+
+/// 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 Debug for Str {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_char('"')?;
+ for c in self.chars() {
+ match c {
+ '\\' => f.write_str(r"\\")?,
+ '"' => f.write_str(r#"\""#)?,
+ '\n' => f.write_str(r"\n")?,
+ '\r' => f.write_str(r"\r")?,
+ '\t' => f.write_str(r"\t")?,
+ _ => f.write_char(c)?,
+ }
+ }
+ 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| err.to_string())
+ }
+}
+
+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,
+ Expected: "string or regular expression",
+ Value::Str(text) => Self::Str(text),
+ @regex: Regex => Self::Regex(regex.clone()),
+}
+
+/// 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,
+ Expected: "start or end",
+ @align: RawAlign => match align {
+ RawAlign::Start => Self::Start,
+ RawAlign::End => Self::End,
+ _ => Err("expected either `start` or `end`")?,
+ },
+}
diff --git a/src/model/styles.rs b/src/model/styles.rs
index 93b615fc..76199ca1 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -107,7 +107,7 @@ impl StyleMap {
/// Mark all contained properties as _scoped_. This means that they only
/// apply to the first descendant node (of their type) in the hierarchy and
/// not its children, too. This is used by
- /// [constructors](crate::eval::Node::construct).
+ /// [constructors](super::Node::construct).
pub fn scoped(mut self) -> Self {
for entry in &mut self.0 {
if let StyleEntry::Property(property) = entry {
diff --git a/src/model/value.rs b/src/model/value.rs
new file mode 100644
index 00000000..4075ce9c
--- /dev/null
+++ b/src/model/value.rs
@@ -0,0 +1,455 @@
+use std::any::Any;
+use std::cmp::Ordering;
+use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
+use std::sync::Arc;
+
+use siphasher::sip128::{Hasher128, SipHasher};
+
+use super::{ops, Args, Array, Cast, Content, Dict, Func, Layout, RawLength, Str};
+use crate::diag::StrResult;
+use crate::geom::{Angle, Color, Em, Fraction, Length, Ratio, Relative, RgbaColor};
+use crate::library::text::RawNode;
+use crate::util::EcoString;
+
+/// 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`.
+ Length(RawLength),
+ /// 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(Relative<RawLength>),
+ /// A fraction: `1fr`.
+ Fraction(Fraction),
+ /// A color value: `#f79143ff`.
+ Color(Color),
+ /// A string: `"string"`.
+ Str(Str),
+ /// 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 dynamic value.
+ Dyn(Dynamic),
+}
+
+impl Value {
+ /// Create a content value from an inline-level node.
+ pub fn inline<T>(node: T) -> Self
+ where
+ T: Layout + Debug + Hash + Sync + Send + 'static,
+ {
+ Self::Content(Content::inline(node))
+ }
+
+ /// Create a content value from a block-level node.
+ pub fn block<T>(node: T) -> Self
+ where
+ T: Layout + Debug + Hash + Sync + Send + 'static,
+ {
+ Self::Content(Content::block(node))
+ }
+
+ /// 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))
+ }
+
+ /// 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(_) => RawLength::TYPE_NAME,
+ Self::Angle(_) => Angle::TYPE_NAME,
+ Self::Ratio(_) => Ratio::TYPE_NAME,
+ Self::Relative(_) => Relative::<RawLength>::TYPE_NAME,
+ Self::Fraction(_) => Fraction::TYPE_NAME,
+ Self::Color(_) => Color::TYPE_NAME,
+ Self::Str(_) => Str::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::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)
+ }
+
+ /// Return the debug representation of the value.
+ pub fn repr(&self) -> Str {
+ format_str!("{:?}", self)
+ }
+
+ /// Return the display representation of the value.
+ pub fn display(self) -> Content {
+ match self {
+ Value::None => Content::new(),
+ Value::Int(v) => Content::Text(format_eco!("{}", v)),
+ Value::Float(v) => Content::Text(format_eco!("{}", v)),
+ Value::Str(v) => Content::Text(v.into()),
+ Value::Content(v) => v,
+
+ // For values which can't be shown "naturally", we return the raw
+ // representation with typst code syntax highlighting.
+ v => Content::show(RawNode { text: v.repr().into(), block: false })
+ .styled(RawNode::LANG, Some("typc".into())),
+ }
+ }
+}
+
+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::Str(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::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::Str(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::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<Length> for Value {
+ fn from(v: Length) -> 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 {
+ if let Some(other) = other.downcast::<Self>() {
+ self == other
+ } else {
+ false
+ }
+ }
+
+ fn dyn_type_name(&self) -> &'static str {
+ T::TYPE_NAME
+ }
+
+ fn hash128(&self) -> u128 {
+ // Also hash the TypeId since nodes 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(format!(
+ "expected {}, found {}",
+ Self::TYPE_NAME,
+ v.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! { RawLength: "length", Length }
+primitive! { Angle: "angle", Angle }
+primitive! { Ratio: "ratio", Ratio }
+primitive! { Relative<RawLength>: "relative length",
+ Relative,
+ Length(v) => v.into(),
+ Ratio(v) => v.into()
+}
+primitive! { Fraction: "fraction", Fraction }
+primitive! { Color: "color", Color }
+primitive! { Str: "string", Str }
+primitive! { Content: "content",
+ Content,
+ None => Content::new(),
+ Str(text) => Content::Text(text.into())
+}
+primitive! { Array: "array", Array }
+primitive! { Dict: "dictionary", Dict }
+primitive! { Func: "function", Func }
+primitive! { Args: "arguments", Args }
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[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(Length::pt(5.5), "5.5pt");
+ test(Angle::deg(90.0), "90deg");
+ test(Ratio::one() / 2.0, "50%");
+ test(
+ Ratio::new(0.3) + RawLength::from(Length::cm(2.0)),
+ "30% + 56.69pt",
+ );
+ test(Fraction::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)");
+
+ // Functions, content and dynamics.
+ test(Content::Text("a".into()), "[...]");
+ test(Func::from_fn("nil", |_, _| Ok(Value::None)), "nil");
+ test(Dynamic::new(1), "1");
+ }
+}
diff --git a/src/model/vm.rs b/src/model/vm.rs
new file mode 100644
index 00000000..a1b1ba81
--- /dev/null
+++ b/src/model/vm.rs
@@ -0,0 +1,86 @@
+use std::path::PathBuf;
+
+use comemo::Tracked;
+
+use super::{Route, Scopes, Value};
+use crate::diag::{SourceError, StrResult};
+use crate::syntax::{SourceId, Span};
+use crate::util::PathExt;
+use crate::World;
+
+/// A virtual machine.
+pub struct Vm<'a> {
+ /// The core context.
+ pub world: Tracked<'a, dyn World>,
+ /// The route of source ids the machine took to reach its current location.
+ pub route: Tracked<'a, Route>,
+ /// The current location.
+ pub location: Option<SourceId>,
+ /// The stack of scopes.
+ pub scopes: Scopes<'a>,
+ /// A control flow event that is currently happening.
+ pub flow: Option<Flow>,
+}
+
+impl<'a> Vm<'a> {
+ /// Create a new virtual machine.
+ pub fn new(
+ world: Tracked<'a, dyn World>,
+ route: Tracked<'a, Route>,
+ location: Option<SourceId>,
+ scopes: Scopes<'a>,
+ ) -> Self {
+ Self {
+ world,
+ route,
+ location,
+ scopes,
+ flow: None,
+ }
+ }
+
+ /// Resolve a user-entered path to be relative to the compilation
+ /// environment's root.
+ pub fn locate(&self, path: &str) -> StrResult<PathBuf> {
+ if let Some(id) = self.location {
+ if let Some(path) = path.strip_prefix('/') {
+ return Ok(self.world.config().root.join(path).normalize());
+ }
+
+ if let Some(dir) = self.world.source(id).path().parent() {
+ return Ok(dir.join(path).normalize());
+ }
+ }
+
+ return 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")
+ }
+ }
+ }
+}