zclsyntax: "expanding" function arguments

This syntax func(arg...) allows the final argument to be a sequence-typed
value that then expands to be one argument for each element of the
value.

This allows applications to define variadic functions where that's
user-friendly while still allowing users to pass tuples to those functions
in situations where the args are chosen dynamically.
This commit is contained in:
Martin Atkins 2017-06-15 08:18:00 -07:00
parent fa06f40141
commit 4ab33cdce0
3 changed files with 126 additions and 7 deletions

View File

@ -103,6 +103,10 @@ type FunctionCallExpr struct {
Name string
Args []Expression
// If true, the final argument should be a tuple, list or set which will
// expand to be one argument per element.
ExpandFinal bool
NameRange zcl.Range
OpenParenRange zcl.Range
CloseParenRange zcl.Range
@ -155,8 +159,60 @@ func (e *FunctionCallExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnosti
params := f.Params()
varParam := f.VarParam()
if len(e.Args) < len(params) {
missing := params[len(e.Args)]
args := e.Args
if e.ExpandFinal {
if len(args) < 1 {
// should never happen if the parser is behaving
panic("ExpandFinal set on function call with no arguments")
}
expandExpr := args[len(args)-1]
expandVal, expandDiags := expandExpr.Value(ctx)
diags = append(diags, expandDiags...)
if expandDiags.HasErrors() {
return cty.DynamicVal, diags
}
switch {
case expandVal.Type().IsTupleType() || expandVal.Type().IsListType() || expandVal.Type().IsSetType():
if expandVal.IsNull() {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid expanding argument value",
Detail: "The expanding argument (indicated by ...) must not be null.",
Context: expandExpr.Range().Ptr(),
Subject: e.Range().Ptr(),
})
return cty.DynamicVal, diags
}
if !expandVal.IsKnown() {
return cty.DynamicVal, diags
}
newArgs := make([]Expression, 0, (len(args)-1)+expandVal.LengthInt())
newArgs = append(newArgs, args[:len(args)-1]...)
it := expandVal.ElementIterator()
for it.Next() {
_, val := it.Element()
newArgs = append(newArgs, &LiteralValueExpr{
Val: val,
SrcRange: expandExpr.Range(),
})
}
args = newArgs
default:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid expanding argument value",
Detail: "The expanding argument (indicated by ...) must be of a tuple, list, or set type.",
Context: expandExpr.Range().Ptr(),
Subject: e.Range().Ptr(),
})
return cty.DynamicVal, diags
}
}
if len(args) < len(params) {
missing := params[len(args)]
qual := ""
if varParam != nil {
qual = " at least"
@ -175,7 +231,7 @@ func (e *FunctionCallExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnosti
}
}
if varParam == nil && len(e.Args) > len(params) {
if varParam == nil && len(args) > len(params) {
return cty.DynamicVal, zcl.Diagnostics{
{
Severity: zcl.DiagError,
@ -184,15 +240,15 @@ func (e *FunctionCallExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnosti
"Function %q expects only %d argument(s).",
e.Name, len(params),
),
Subject: e.Args[len(params)].StartRange().Ptr(),
Subject: args[len(params)].StartRange().Ptr(),
Context: e.Range().Ptr(),
},
}
}
argVals := make([]cty.Value, len(e.Args))
argVals := make([]cty.Value, len(args))
for i, argExpr := range e.Args {
for i, argExpr := range args {
var param *function.Parameter
if i < len(params) {
param = &params[i]

View File

@ -242,6 +242,46 @@ upper(
cty.StringVal("FOO"),
0,
},
{
`upper(["foo"]...)`,
&zcl.EvalContext{
Functions: map[string]function.Function{
"upper": stdlib.UpperFunc,
},
},
cty.StringVal("FOO"),
0,
},
{
`upper("foo", []...)`,
&zcl.EvalContext{
Functions: map[string]function.Function{
"upper": stdlib.UpperFunc,
},
},
cty.StringVal("FOO"),
0,
},
{
`upper("foo", "bar")`,
&zcl.EvalContext{
Functions: map[string]function.Function{
"upper": stdlib.UpperFunc,
},
},
cty.DynamicVal,
1, // too many function arguments
},
{
`upper(["foo", "bar"]...)`,
&zcl.EvalContext{
Functions: map[string]function.Function{
"upper": stdlib.UpperFunc,
},
},
cty.DynamicVal,
1, // too many function arguments
},
{
`[]`,
nil,

View File

@ -779,6 +779,7 @@ func (p *parser) finishParsingFunctionCall(name Token) (Expression, zcl.Diagnost
var args []Expression
var diags zcl.Diagnostics
var expandFinal bool
var closeTok Token
// Arbitrary newlines are allowed inside the function call parentheses.
@ -810,6 +811,26 @@ Token:
break Token
}
if sep.Type == TokenEllipsis {
expandFinal = true
if p.Peek().Type != TokenCParen {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing closing parenthesis",
Detail: "An expanded function argument (with ...) must be immediately followed by closing parentheses.",
Subject: &sep.Range,
Context: zcl.RangeBetween(name.Range, sep.Range).Ptr(),
})
}
closeTok = p.recover(TokenCParen)
} else {
closeTok = p.Read() // eat closing paren
}
break Token
}
if sep.Type != TokenComma {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
@ -818,7 +839,7 @@ Token:
Subject: &sep.Range,
Context: zcl.RangeBetween(name.Range, sep.Range).Ptr(),
})
p.recover(TokenCParen)
closeTok = p.recover(TokenCParen)
break Token
}
@ -836,6 +857,8 @@ Token:
Name: string(name.Bytes),
Args: args,
ExpandFinal: expandFinal,
NameRange: name.Range,
OpenParenRange: openTok.Range,
CloseParenRange: closeTok.Range,