mirror of
https://github.com/typst/typst
synced 2025-06-28 08:12:53 +08:00
Argument collection and spreading
This commit is contained in:
parent
144f208821
commit
6a3385e4e7
@ -60,7 +60,7 @@ impl PartialEq for Function {
|
||||
pub struct FuncArgs {
|
||||
/// The span of the whole argument list.
|
||||
pub span: Span,
|
||||
/// The positional arguments.
|
||||
/// The positional and named arguments.
|
||||
pub items: Vec<FuncArg>,
|
||||
}
|
||||
|
||||
@ -118,20 +118,28 @@ impl FuncArgs {
|
||||
where
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
let index = match self
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|arg| arg.name.as_deref())
|
||||
.position(|other| name == other)
|
||||
{
|
||||
Some(index) => index,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let value = self.items.remove(index).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)
|
||||
}
|
||||
|
||||
T::cast(value).map(Some).at(span)
|
||||
/// 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
|
||||
|
142
src/eval/mod.rs
142
src/eval/mod.rs
@ -386,33 +386,49 @@ impl Eval for CallArgs {
|
||||
type Output = FuncArgs;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
Ok(FuncArgs {
|
||||
span: self.span,
|
||||
items: self
|
||||
.items
|
||||
.iter()
|
||||
.map(|arg| arg.eval(ctx))
|
||||
.collect::<TypResult<Vec<_>>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut items = Vec::with_capacity(self.items.len());
|
||||
|
||||
impl Eval for CallArg {
|
||||
type Output = FuncArg;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
Ok(match self {
|
||||
Self::Pos(expr) => FuncArg {
|
||||
span: self.span(),
|
||||
for arg in &self.items {
|
||||
let span = arg.span();
|
||||
match arg {
|
||||
CallArg::Pos(expr) => {
|
||||
items.push(FuncArg {
|
||||
span,
|
||||
name: None,
|
||||
value: Spanned::new(expr.eval(ctx)?, expr.span()),
|
||||
},
|
||||
Self::Named(Named { name, expr }) => FuncArg {
|
||||
span: self.span(),
|
||||
});
|
||||
}
|
||||
CallArg::Named(Named { name, expr }) => {
|
||||
items.push(FuncArg {
|
||||
span,
|
||||
name: Some(name.string.clone()),
|
||||
value: Spanned::new(expr.eval(ctx)?, expr.span()),
|
||||
});
|
||||
}
|
||||
CallArg::Spread(expr) => match expr.eval(ctx)? {
|
||||
Value::Args(args) => {
|
||||
items.extend(args.items.iter().cloned());
|
||||
}
|
||||
Value::Array(array) => {
|
||||
items.extend(array.into_iter().map(|value| FuncArg {
|
||||
span,
|
||||
name: None,
|
||||
value: Spanned::new(value, span),
|
||||
}));
|
||||
}
|
||||
Value::Dict(dict) => {
|
||||
items.extend(dict.into_iter().map(|(key, value)| FuncArg {
|
||||
span,
|
||||
name: Some(key),
|
||||
value: Spanned::new(value, span),
|
||||
}));
|
||||
}
|
||||
v => bail!(expr.span(), "cannot spread {}", v.type_name()),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FuncArgs { span: self.span, items })
|
||||
}
|
||||
}
|
||||
|
||||
@ -420,25 +436,7 @@ impl Eval for ClosureExpr {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
struct FuncParam {
|
||||
name: EcoString,
|
||||
default: Option<Value>,
|
||||
}
|
||||
|
||||
// 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<_>>()?;
|
||||
let name = self.name.as_ref().map(|name| name.string.clone());
|
||||
|
||||
// Collect captured variables.
|
||||
let captured = {
|
||||
@ -447,10 +445,30 @@ impl Eval for ClosureExpr {
|
||||
visitor.finish()
|
||||
};
|
||||
|
||||
let mut sink = None;
|
||||
let mut params = Vec::with_capacity(self.params.len());
|
||||
|
||||
// Collect parameters and an optional sink parameter.
|
||||
for param in &self.params {
|
||||
match param {
|
||||
ClosureParam::Pos(name) => {
|
||||
params.push((name.string.clone(), None));
|
||||
}
|
||||
ClosureParam::Named(Named { name, expr }) => {
|
||||
params.push((name.string.clone(), Some(expr.eval(ctx)?)));
|
||||
}
|
||||
ClosureParam::Sink(name) => {
|
||||
if sink.is_some() {
|
||||
bail!(name.span, "only one argument sink is allowed");
|
||||
}
|
||||
sink = Some(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);
|
||||
let name = self.name.as_ref().map(|name| name.string.clone());
|
||||
|
||||
// Define the actual function.
|
||||
let func = Function::new(name, move |ctx, args| {
|
||||
@ -460,19 +478,21 @@ impl Eval for ClosureExpr {
|
||||
ctx.scopes.top = captured.clone();
|
||||
|
||||
// Parse the arguments according to the parameter list.
|
||||
for param in ¶ms {
|
||||
let value = match ¶m.default {
|
||||
None => args.expect::<Value>(¶m.name)?,
|
||||
Some(default) => args
|
||||
.named::<Value>(¶m.name)?
|
||||
.unwrap_or_else(|| default.clone()),
|
||||
};
|
||||
for (param, default) in ¶ms {
|
||||
ctx.scopes.def_mut(param, match default {
|
||||
None => args.expect::<Value>(param)?,
|
||||
Some(default) => {
|
||||
args.named::<Value>(param)?.unwrap_or_else(|| default.clone())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ctx.scopes.def_mut(¶m.name, value);
|
||||
// Put the remaining arguments into the sink.
|
||||
if let Some(sink) = &sink {
|
||||
ctx.scopes.def_mut(sink, Rc::new(args.take()));
|
||||
}
|
||||
|
||||
let value = body.eval(ctx)?;
|
||||
|
||||
ctx.scopes = prev_scopes;
|
||||
Ok(value)
|
||||
});
|
||||
@ -486,27 +506,11 @@ impl Eval for WithExpr {
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
let callee = self.callee.eval(ctx)?.cast::<Function>().at(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
|
||||
.items
|
||||
.iter()
|
||||
.filter(|arg| {
|
||||
arg.name.is_none()
|
||||
|| args.items.iter().all(|other| arg.name != other.name)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Preprend the applied arguments so that the positional arguments
|
||||
// are in the right order.
|
||||
args.items.splice(.. 0, kept);
|
||||
|
||||
// Call the original function.
|
||||
args.items.splice(.. 0, applied.items.iter().cloned());
|
||||
callee(ctx, args)
|
||||
});
|
||||
|
||||
@ -726,9 +730,7 @@ impl Access for CallExpr {
|
||||
|
||||
RefMut::try_map(guard, |value| match value {
|
||||
Value::Array(array) => array.get_mut(args.into_index()?).at(self.span),
|
||||
|
||||
Value::Dict(dict) => Ok(dict.get_mut(args.into_key()?)),
|
||||
|
||||
v => bail!(
|
||||
self.callee.span(),
|
||||
"expected collection, found {}",
|
||||
|
@ -3,7 +3,7 @@ use std::cmp::Ordering;
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::{ops, Array, Dict, Function, Template, TemplateFunc};
|
||||
use super::{ops, Array, Dict, FuncArgs, Function, Template, TemplateFunc};
|
||||
use crate::color::{Color, RgbaColor};
|
||||
use crate::diag::StrResult;
|
||||
use crate::exec::ExecContext;
|
||||
@ -48,6 +48,8 @@ pub enum Value {
|
||||
Func(Function),
|
||||
/// A dynamic value.
|
||||
Dyn(Dynamic),
|
||||
/// Captured arguments to a function.
|
||||
Args(Rc<FuncArgs>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
@ -78,6 +80,7 @@ impl Value {
|
||||
Self::Dict(_) => Dict::TYPE_NAME,
|
||||
Self::Template(_) => Template::TYPE_NAME,
|
||||
Self::Func(_) => Function::TYPE_NAME,
|
||||
Self::Args(_) => Rc::<FuncArgs>::TYPE_NAME,
|
||||
Self::Dyn(v) => v.type_name(),
|
||||
}
|
||||
}
|
||||
@ -387,4 +390,5 @@ primitive! { Array: "array", Array }
|
||||
primitive! { Dict: "dictionary", Dict }
|
||||
primitive! { Template: "template", Template }
|
||||
primitive! { Function: "function", Func }
|
||||
primitive! { Rc<FuncArgs>: "arguments", Args }
|
||||
primitive! { f64: "float", Float, Int(v) => v as f64 }
|
||||
|
@ -406,6 +406,10 @@ fn parenthesized(p: &mut Parser) -> Option<Expr> {
|
||||
},
|
||||
[CallArg::Pos(_), ..] => array(p, items, span),
|
||||
[CallArg::Named(_), ..] => dict(p, items, span),
|
||||
[CallArg::Spread(expr), ..] => {
|
||||
p.error(expr.span(), "spreading is not allowed here");
|
||||
return None;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -443,6 +447,10 @@ fn collection(p: &mut Parser) -> (Vec<CallArg>, bool) {
|
||||
|
||||
/// Parse an expression or a named pair.
|
||||
fn item(p: &mut Parser) -> Option<CallArg> {
|
||||
if p.eat_if(Token::Dots) {
|
||||
return expr(p).map(CallArg::Spread);
|
||||
}
|
||||
|
||||
let first = expr(p)?;
|
||||
if p.eat_if(Token::Colon) {
|
||||
if let Expr::Ident(name) = first {
|
||||
@ -457,7 +465,8 @@ fn item(p: &mut Parser) -> Option<CallArg> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a collection into an array, producing errors for named items.
|
||||
/// Convert a collection into an array, producing errors for anything other than
|
||||
/// expressions.
|
||||
fn array(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
|
||||
let iter = items.into_iter().filter_map(|item| match item {
|
||||
CallArg::Pos(expr) => Some(expr),
|
||||
@ -465,11 +474,16 @@ fn array(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
|
||||
p.error(item.span(), "expected expression, found named pair");
|
||||
None
|
||||
}
|
||||
CallArg::Spread(_) => {
|
||||
p.error(item.span(), "spreading is not allowed here");
|
||||
None
|
||||
}
|
||||
});
|
||||
Expr::Array(ArrayExpr { span, items: iter.collect() })
|
||||
}
|
||||
|
||||
/// Convert a collection into a dictionary, producing errors for expressions.
|
||||
/// Convert a collection into a dictionary, producing errors for anything other
|
||||
/// than named pairs.
|
||||
fn dict(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
|
||||
let iter = items.into_iter().filter_map(|item| match item {
|
||||
CallArg::Named(named) => Some(named),
|
||||
@ -477,18 +491,23 @@ fn dict(p: &mut Parser, items: Vec<CallArg>, span: Span) -> Expr {
|
||||
p.error(item.span(), "expected named pair, found expression");
|
||||
None
|
||||
}
|
||||
CallArg::Spread(_) => {
|
||||
p.error(item.span(), "spreading is not allowed here");
|
||||
None
|
||||
}
|
||||
});
|
||||
Expr::Dict(DictExpr { span, items: iter.collect() })
|
||||
}
|
||||
|
||||
/// Convert a collection into a list of parameters, producing errors for
|
||||
/// anything other than identifiers and named pairs.
|
||||
/// anything other than identifiers, spread operations 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::Pos(Expr::Ident(ident)) => Some(ClosureParam::Pos(ident)),
|
||||
CallArg::Named(named) => Some(ClosureParam::Named(named)),
|
||||
CallArg::Spread(Expr::Ident(ident)) => Some(ClosureParam::Sink(ident)),
|
||||
_ => {
|
||||
p.error(item.span(), "expected parameter");
|
||||
p.error(item.span(), "expected identifier");
|
||||
None
|
||||
}
|
||||
});
|
||||
|
@ -357,6 +357,10 @@ impl Pretty for CallArg {
|
||||
match self {
|
||||
Self::Pos(expr) => expr.pretty(p),
|
||||
Self::Named(named) => named.pretty(p),
|
||||
Self::Spread(expr) => {
|
||||
p.push_str("..");
|
||||
expr.pretty(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -375,6 +379,10 @@ impl Pretty for ClosureParam {
|
||||
match self {
|
||||
Self::Pos(ident) => ident.pretty(p),
|
||||
Self::Named(named) => named.pretty(p),
|
||||
Self::Sink(ident) => {
|
||||
p.push_str("..");
|
||||
ident.pretty(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -496,6 +504,7 @@ impl Pretty for Value {
|
||||
Self::Dict(v) => v.pretty(p),
|
||||
Self::Template(v) => v.pretty(p),
|
||||
Self::Func(v) => v.pretty(p),
|
||||
Self::Args(v) => v.pretty(p),
|
||||
Self::Dyn(v) => v.pretty(p),
|
||||
}
|
||||
}
|
||||
|
@ -408,13 +408,15 @@ pub struct CallArgs {
|
||||
pub items: Vec<CallArg>,
|
||||
}
|
||||
|
||||
/// An argument to a function call: `12` or `draw: false`.
|
||||
/// An argument to a function call.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CallArg {
|
||||
/// A positional argument.
|
||||
/// A positional argument: `12`.
|
||||
Pos(Expr),
|
||||
/// A named argument.
|
||||
/// A named argument: `draw: false`.
|
||||
Named(Named),
|
||||
/// A spreaded argument: `..things`.
|
||||
Spread(Expr),
|
||||
}
|
||||
|
||||
impl CallArg {
|
||||
@ -423,6 +425,7 @@ impl CallArg {
|
||||
match self {
|
||||
Self::Pos(expr) => expr.span(),
|
||||
Self::Named(named) => named.span(),
|
||||
Self::Spread(expr) => expr.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -442,13 +445,26 @@ pub struct ClosureExpr {
|
||||
pub body: Rc<Expr>,
|
||||
}
|
||||
|
||||
/// An parameter to a closure: `x` or `draw: false`.
|
||||
/// An parameter to a closure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ClosureParam {
|
||||
/// A positional parameter.
|
||||
/// A positional parameter: `x`.
|
||||
Pos(Ident),
|
||||
/// A named parameter with a default value.
|
||||
/// A named parameter with a default value: `draw: false`.
|
||||
Named(Named),
|
||||
/// A parameter sink: `..args`.
|
||||
Sink(Ident),
|
||||
}
|
||||
|
||||
impl ClosureParam {
|
||||
/// The source code location.
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
Self::Pos(ident) => ident.span,
|
||||
Self::Named(named) => named.span(),
|
||||
Self::Sink(ident) => ident.span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A with expression: `f with (x, y: 1)`.
|
||||
|
@ -202,6 +202,7 @@ impl_visitors! {
|
||||
match arg {
|
||||
CallArg::Pos(expr) => v.visit_expr(expr),
|
||||
CallArg::Named(named) => v.visit_expr(r!(named.expr)),
|
||||
CallArg::Spread(expr) => v.visit_expr(expr),
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,6 +220,7 @@ impl_visitors! {
|
||||
v.visit_binding(r!(named.name));
|
||||
v.visit_expr(r!(named.expr));
|
||||
}
|
||||
ClosureParam::Sink(binding) => v.visit_binding(binding),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
//! An economical string.
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::cmp::Ordering;
|
||||
use std::convert::TryFrom;
|
||||
|
71
tests/typ/code/spread.typ
Normal file
71
tests/typ/code/spread.typ
Normal file
@ -0,0 +1,71 @@
|
||||
// Test argument sinks and spreading.
|
||||
// Ref: false
|
||||
|
||||
---
|
||||
// Test standard argument overriding.
|
||||
{
|
||||
let font(style: normal, weight: regular) = {
|
||||
"(style: " + repr(style) + ", weight: " + repr(weight) + ")"
|
||||
}
|
||||
|
||||
let myfont(..args) = font(weight: bold, ..args)
|
||||
test(myfont(), "(style: normal, weight: bold)")
|
||||
test(myfont(weight: 100), "(style: normal, weight: 100)")
|
||||
test(myfont(style: italic), "(style: italic, weight: bold)")
|
||||
}
|
||||
|
||||
---
|
||||
// Test multiple calls.
|
||||
{
|
||||
let f(b, c: "!") = b + c
|
||||
let g(a, ..sink) = a + f(..sink)
|
||||
test(g("a", "b", c: "c"), "abc")
|
||||
}
|
||||
|
||||
---
|
||||
// Test storing arguments in a variable.
|
||||
{
|
||||
let args
|
||||
let save(..sink) = {
|
||||
args = sink
|
||||
}
|
||||
|
||||
save(1, 2, three: true)
|
||||
test(type(args), "arguments")
|
||||
test(repr(args), "(1, 2, three: true)")
|
||||
}
|
||||
|
||||
---
|
||||
// Test spreading array and dictionary.
|
||||
{
|
||||
let more = (3, -3, 6, 10)
|
||||
test(min(1, 2, ..more), -3)
|
||||
test(max(..more, 9), 10)
|
||||
test(max(..more, 11), 11)
|
||||
}
|
||||
|
||||
{
|
||||
let more = (c: 3, d: 4)
|
||||
let tostr(..args) = repr(args)
|
||||
test(tostr(a: 1, ..more, b: 2), "(a: 1, c: 3, d: 4, b: 2)")
|
||||
}
|
||||
|
||||
---
|
||||
// Error: 8-14 cannot spread string
|
||||
#min(.."nope")
|
||||
|
||||
---
|
||||
// Error: 10-14 expected identifier
|
||||
#let f(..true) = none
|
||||
|
||||
---
|
||||
// Error: 15-16 only one argument sink is allowed
|
||||
#let f(..a, ..b) = none
|
||||
|
||||
---
|
||||
// Error: 5-6 spreading is not allowed here
|
||||
{(..x)}
|
||||
|
||||
---
|
||||
// Error: 11-17 spreading is not allowed here
|
||||
{(1, 2, ..(1, 2))}
|
Loading…
x
Reference in New Issue
Block a user