diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-10-17 19:26:24 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-10-17 20:04:22 +0200 |
| commit | e21822665591dc19766275da1e185215a6b945ef (patch) | |
| tree | 7788e211c3c33c8b5a8ad7d5eb7574e33631eb16 /src/model | |
| parent | 4fd031a256b2ecfe524859d5599fafb386395572 (diff) | |
Merge some modules
Diffstat (limited to 'src/model')
| -rw-r--r-- | src/model/args.rs | 225 | ||||
| -rw-r--r-- | src/model/array.rs | 369 | ||||
| -rw-r--r-- | src/model/capture.rs | 186 | ||||
| -rw-r--r-- | src/model/cast.rs | 360 | ||||
| -rw-r--r-- | src/model/content.rs | 31 | ||||
| -rw-r--r-- | src/model/dict.rs | 193 | ||||
| -rw-r--r-- | src/model/eval.rs | 1212 | ||||
| -rw-r--r-- | src/model/func.rs | 249 | ||||
| -rw-r--r-- | src/model/layout.rs | 17 | ||||
| -rw-r--r-- | src/model/methods.rs | 167 | ||||
| -rw-r--r-- | src/model/mod.rs | 34 | ||||
| -rw-r--r-- | src/model/ops.rs | 398 | ||||
| -rw-r--r-- | src/model/property.rs | 3 | ||||
| -rw-r--r-- | src/model/raw.rs | 293 | ||||
| -rw-r--r-- | src/model/recipe.rs | 6 | ||||
| -rw-r--r-- | src/model/scope.rs | 161 | ||||
| -rw-r--r-- | src/model/show.rs | 3 | ||||
| -rw-r--r-- | src/model/str.rs | 470 | ||||
| -rw-r--r-- | src/model/styles.rs | 2 | ||||
| -rw-r--r-- | src/model/value.rs | 455 | ||||
| -rw-r--r-- | src/model/vm.rs | 86 |
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") + } + } + } +} |
