summaryrefslogtreecommitdiff
path: root/src/model/func.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/model/func.rs')
-rw-r--r--src/model/func.rs249
1 files changed, 249 insertions, 0 deletions
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
+ }
+}