diff --git a/zcl/zclsyntax/expression.go b/zcl/zclsyntax/expression.go index 2ffda27..953b3b6 100644 --- a/zcl/zclsyntax/expression.go +++ b/zcl/zclsyntax/expression.go @@ -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 = ¶ms[i] diff --git a/zcl/zclsyntax/expression_test.go b/zcl/zclsyntax/expression_test.go index 44d6193..3b45752 100644 --- a/zcl/zclsyntax/expression_test.go +++ b/zcl/zclsyntax/expression_test.go @@ -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, diff --git a/zcl/zclsyntax/parser.go b/zcl/zclsyntax/parser.go index 306b47a..22ce109 100644 --- a/zcl/zclsyntax/parser.go +++ b/zcl/zclsyntax/parser.go @@ -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,