diff --git a/src/eval/mod.rs b/src/eval/mod.rs index abb7cab7a..1b851a9bc 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, + /// 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 { - let file = ctx.source; - let params = Rc::clone(&self.params); - let body = Rc::clone(&self.body); + struct FuncParam { + name: EcoString, + default: Option, + } - // 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::>()?; + + // 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::(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 ¶ms { + let value = match ¶m.default { + None => args.expect::(¶m.name)?, + Some(default) => args + .named::(¶m.name)? + .unwrap_or_else(|| default.clone()), + }; - let result = body.eval(ctx); + ctx.scopes.def_mut(¶m.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::() .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 9eabcfc75..9678b1293 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -312,7 +312,7 @@ fn primary(p: &mut Parser, atomic: bool) -> Option { 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 { // 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 { /// Convert a collection into an array, producing errors for named items. fn array(p: &mut Parser, items: Vec, 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, 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) -> Vec { + 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) -> Vec { - 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 { 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 bd388b567..523005462 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 cf9aff4af..9292d5b6c 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, /// The parameter bindings. - pub params: Rc>, + pub params: Vec, /// The body of the closure. pub body: Rc, } +/// 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 81cba5c78..bc9359b3f 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 1bc369e95..3b8b42619 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)) +}