summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-08-12 16:00:09 +0200
committerLaurenz <laurmaedje@gmail.com>2021-08-12 16:07:42 +0200
commitd002cdf451e1c6efbf7cd7f2303264526b6f8a92 (patch)
tree31b8446728a93550cab57d50bba92eb32f280678
parentccb4be4da4e8aeda115b22f2a0b586a86f5e31bd (diff)
Named arguments for user defined functions
-rw-r--r--src/eval/mod.rs63
-rw-r--r--src/parse/mod.rs42
-rw-r--r--src/pretty.rs9
-rw-r--r--src/syntax/expr.rs11
-rw-r--r--src/syntax/visit.rs24
-rw-r--r--tests/typ/code/closure.typ14
6 files changed, 126 insertions, 37 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index abb7cab7..1b851a9b 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -71,12 +71,12 @@ pub struct EvalContext<'a> {
pub images: &'a mut ImageStore,
/// Caches evaluated modules.
pub modules: &'a mut ModuleCache,
- /// The active scopes.
- pub scopes: Scopes<'a>,
/// The id of the currently evaluated source file.
pub source: SourceId,
/// The stack of imported files that led to evaluation of the current file.
pub route: Vec<SourceId>,
+ /// The active scopes.
+ pub scopes: Scopes<'a>,
/// The expression map for the currently built template.
pub map: ExprMap,
}
@@ -89,9 +89,9 @@ impl<'a> EvalContext<'a> {
sources: &mut ctx.sources,
images: &mut ctx.images,
modules: &mut ctx.modules,
- scopes: Scopes::new(Some(&ctx.std)),
source,
route: vec![],
+ scopes: Scopes::new(Some(&ctx.std)),
map: ExprMap::new(),
}
}
@@ -439,31 +439,66 @@ impl Eval for ClosureExpr {
type Output = Value;
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
+ struct FuncParam {
+ name: EcoString,
+ default: Option<Value>,
+ }
+
let file = ctx.source;
- let params = Rc::clone(&self.params);
- let body = Rc::clone(&self.body);
+ let name = self.name.as_ref().map(|name| name.string.clone());
- // Collect the captured variables.
+ // Evaluate default values for named parameters.
+ let params: Vec<_> = self
+ .params
+ .iter()
+ .map(|param| match param {
+ ClosureParam::Pos(name) => {
+ Ok(FuncParam { name: name.string.clone(), default: None })
+ }
+ ClosureParam::Named(Named { name, expr }) => Ok(FuncParam {
+ name: name.string.clone(),
+ default: Some(expr.eval(ctx)?),
+ }),
+ })
+ .collect::<TypResult<_>>()?;
+
+ // Collect captured variables.
let captured = {
let mut visitor = CapturesVisitor::new(&ctx.scopes);
visitor.visit_closure(self);
visitor.finish()
};
- let name = self.name.as_ref().map(|name| name.string.clone());
+ // Clone the body expression so that we don't have a lifetime
+ // dependence on the AST.
+ let body = Rc::clone(&self.body);
+
+ // Define the actual function.
let func = Function::new(name, move |ctx, args| {
+ let prev_file = mem::replace(&mut ctx.source, file);
+
// Don't leak the scopes from the call site. Instead, we use the
// scope of captured variables we collected earlier.
let prev_scopes = mem::take(&mut ctx.scopes);
- let prev_file = mem::replace(&mut ctx.source, file);
ctx.scopes.top = captured.clone();
- for param in params.iter() {
- let value = args.expect::<Value>(param.as_str())?;
- ctx.scopes.def_mut(param.as_str(), value);
- }
+ let mut try_eval = || {
+ // Parse the arguments according to the parameter list.
+ for param in &params {
+ let value = match &param.default {
+ None => args.expect::<Value>(&param.name)?,
+ Some(default) => args
+ .named::<Value>(&param.name)?
+ .unwrap_or_else(|| default.clone()),
+ };
+
+ ctx.scopes.def_mut(&param.name, value);
+ }
- let result = body.eval(ctx);
+ body.eval(ctx)
+ };
+
+ let result = try_eval();
ctx.scopes = prev_scopes;
ctx.source = prev_file;
result
@@ -483,9 +518,9 @@ impl Eval for WithExpr {
.cast::<Function>()
.map_err(Error::at(ctx.source, self.callee.span()))?;
+ let name = callee.name().cloned();
let applied = self.args.eval(ctx)?;
- let name = callee.name().cloned();
let func = Function::new(name, move |ctx, args| {
// Remove named arguments that were overridden.
let kept: Vec<_> = applied
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index 9eabcfc7..9678b129 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -312,7 +312,7 @@ fn primary(p: &mut Parser, atomic: bool) -> Option<Expr> {
Expr::Closure(ClosureExpr {
span: ident.span.join(body.span()),
name: None,
- params: Rc::new(vec![ident]),
+ params: vec![ClosureParam::Pos(ident)],
body: Rc::new(body),
})
} else {
@@ -385,12 +385,12 @@ fn parenthesized(p: &mut Parser) -> Option<Expr> {
// Arrow means this is a closure's parameter list.
if p.eat_if(Token::Arrow) {
- let params = idents(p, items);
+ let params = params(p, items);
let body = expr(p)?;
return Some(Expr::Closure(ClosureExpr {
span: span.join(body.span()),
name: None,
- params: Rc::new(params),
+ params,
body: Rc::new(body),
}));
}
@@ -459,41 +459,53 @@ fn item(p: &mut Parser) -> Option<CallArg> {
/// Convert a collection into an array, producing errors for named items.
fn array(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
- let items = items.into_iter().filter_map(|item| match item {
+ let iter = items.into_iter().filter_map(|item| match item {
CallArg::Pos(expr) => Some(expr),
CallArg::Named(_) => {
p.error(item.span(), "expected expression, found named pair");
None
}
});
-
- Expr::Array(ArrayExpr { span, items: items.collect() })
+ Expr::Array(ArrayExpr { span, items: iter.collect() })
}
/// Convert a collection into a dictionary, producing errors for expressions.
fn dict(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
- let items = items.into_iter().filter_map(|item| match item {
+ let iter = items.into_iter().filter_map(|item| match item {
CallArg::Named(named) => Some(named),
CallArg::Pos(_) => {
p.error(item.span(), "expected named pair, found expression");
None
}
});
+ Expr::Dict(DictExpr { span, items: iter.collect() })
+}
- Expr::Dict(DictExpr { span, items: items.collect() })
+/// Convert a collection into a list of parameters, producing errors for
+/// anything other than identifiers and named pairs.
+fn params(p: &mut Parser, items: Vec<CallArg>) -> Vec<ClosureParam> {
+ let iter = items.into_iter().filter_map(|item| match item {
+ CallArg::Pos(Expr::Ident(id)) => Some(ClosureParam::Pos(id)),
+ CallArg::Named(named) => Some(ClosureParam::Named(named)),
+ _ => {
+ p.error(item.span(), "expected parameter");
+ None
+ }
+ });
+ iter.collect()
}
/// Convert a collection into a list of identifiers, producing errors for
/// anything other than identifiers.
fn idents(p: &mut Parser, items: Vec<CallArg>) -> Vec<Ident> {
- let items = items.into_iter().filter_map(|item| match item {
+ let iter = items.into_iter().filter_map(|item| match item {
CallArg::Pos(Expr::Ident(id)) => Some(id),
_ => {
p.error(item.span(), "expected identifier");
None
}
});
- items.collect()
+ iter.collect()
}
// Parse a template value: `[...]`.
@@ -594,28 +606,28 @@ fn let_expr(p: &mut Parser) -> Option<Expr> {
init = with_expr(p, Expr::Ident(binding.clone()));
} else {
// If a parenthesis follows, this is a function definition.
- let mut params = None;
+ let mut maybe_params = None;
if p.peek_direct() == Some(Token::LeftParen) {
p.start_group(Group::Paren, TokenMode::Code);
let items = collection(p).0;
- params = Some(idents(p, items));
+ maybe_params = Some(params(p, items));
p.end_group();
}
if p.eat_if(Token::Eq) {
init = expr(p);
- } else if params.is_some() {
+ } else if maybe_params.is_some() {
// Function definitions must have a body.
p.expected_at(p.prev_end(), "body");
}
// Rewrite into a closure expression if it's a function definition.
- if let Some(params) = params {
+ if let Some(params) = maybe_params {
let body = init?;
init = Some(Expr::Closure(ClosureExpr {
span: binding.span.join(body.span()),
name: Some(binding.clone()),
- params: Rc::new(params),
+ params,
body: Rc::new(body),
}));
}
diff --git a/src/pretty.rs b/src/pretty.rs
index bd388b56..52300546 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -370,6 +370,15 @@ impl Pretty for ClosureExpr {
}
}
+impl Pretty for ClosureParam {
+ fn pretty(&self, p: &mut Printer) {
+ match self {
+ Self::Pos(ident) => ident.pretty(p),
+ Self::Named(named) => named.pretty(p),
+ }
+ }
+}
+
impl Pretty for WithExpr {
fn pretty(&self, p: &mut Printer) {
self.callee.pretty(p);
diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs
index cf9aff4a..9292d5b6 100644
--- a/src/syntax/expr.rs
+++ b/src/syntax/expr.rs
@@ -437,11 +437,20 @@ pub struct ClosureExpr {
/// This only exists if you use the function syntax sugar: `let f(x) = y`.
pub name: Option<Ident>,
/// The parameter bindings.
- pub params: Rc<Vec<Ident>>,
+ pub params: Vec<ClosureParam>,
/// The body of the closure.
pub body: Rc<Expr>,
}
+/// An parameter to a closure: `x` or `draw: false`.
+#[derive(Debug, Clone, PartialEq)]
+pub enum ClosureParam {
+ /// A positional parameter.
+ Pos(Ident),
+ /// A named parameter with a default value.
+ Named(Named),
+}
+
/// A with expression: `f with (x, y: 1)`.
///
/// Applies arguments to a function.
diff --git a/src/syntax/visit.rs b/src/syntax/visit.rs
index 81cba5c7..bc9359b3 100644
--- a/src/syntax/visit.rs
+++ b/src/syntax/visit.rs
@@ -192,13 +192,6 @@ impl_visitors! {
v.visit_args(r!(call.args));
}
- visit_closure(v, closure: ClosureExpr) {
- for param in r!(rc: closure.params) {
- v.visit_binding(param);
- }
- v.visit_expr(r!(rc: closure.body));
- }
-
visit_args(v, args: CallArgs) {
for arg in r!(args.items) {
v.visit_arg(arg);
@@ -212,6 +205,23 @@ impl_visitors! {
}
}
+ visit_closure(v, closure: ClosureExpr) {
+ for param in r!(closure.params) {
+ v.visit_param(param);
+ }
+ v.visit_expr(r!(rc: closure.body));
+ }
+
+ visit_param(v, param: ClosureParam) {
+ match param {
+ ClosureParam::Pos(binding) => v.visit_binding(binding),
+ ClosureParam::Named(named) => {
+ v.visit_binding(r!(named.name));
+ v.visit_expr(r!(named.expr));
+ }
+ }
+ }
+
visit_with(v, with_expr: WithExpr) {
v.visit_expr(r!(with_expr.callee));
v.visit_args(r!(with_expr.args));
diff --git a/tests/typ/code/closure.typ b/tests/typ/code/closure.typ
index 1bc369e9..3b8b4261 100644
--- a/tests/typ/code/closure.typ
+++ b/tests/typ/code/closure.typ
@@ -83,3 +83,17 @@
// Error: 8-13 unexpected argument
f(1, "two", () => x)
}
+
+---
+// Named arguments.
+{
+ let greet(name, birthday: false) = {
+ if birthday { "Happy Birthday, " } else { "Hey, " } + name + "!"
+ }
+
+ test(greet("Typst"), "Hey, Typst!")
+ test(greet("Typst", birthday: true), "Happy Birthday, Typst!")
+
+ // Error: 23-35 unexpected argument
+ test(greet("Typst", whatever: 10))
+}