Named arguments for user defined functions

This commit is contained in:
Laurenz 2021-08-12 16:00:09 +02:00
parent ccb4be4da4
commit d002cdf451
6 changed files with 127 additions and 38 deletions

View File

@ -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> {
let file = ctx.source;
let params = Rc::clone(&self.params);
let body = Rc::clone(&self.body);
struct FuncParam {
name: EcoString,
default: Option<Value>,
}
// Collect the captured variables.
let file = ctx.source;
let name = self.name.as_ref().map(|name| name.string.clone());
// 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()),
};
let result = body.eval(ctx);
ctx.scopes.def_mut(&param.name, value);
}
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

View File

@ -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),
}));
}

View File

@ -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);

View File

@ -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.

View File

@ -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));

View File

@ -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))
}