Repository: CloudyKit/jet (go)

Issue:
Add function capturing syntax
From https://github.com/CloudyKit/jet/issues/82#issuecomment-653627407:

> would be kinda cool to do something like [Gleam's function capture](https://gleam.run/tour/functions.html#function-capturing) where you can specify what position you want the argument in, so like `{{ foo()|bar(_, param2, param3)|baz(param1, _, param3) }}` (Gleam probably took that from somewhere, but that's just where I've seen it recently.)

cc @tooolbox

Pull request:
add support for function capturing syntax
closes #162

Patch:
diff --git a/constructors.go b/constructors.go
index 8f3ed99..d0f023b 100644
--- a/constructors.go
+++ b/constructors.go
@@ -239,6 +239,6 @@ func (t *Template) newIdentifier(ident string, pos Pos, line int) *IdentifierNod
 	return &IdentifierNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeIdentifier, Pos: pos, Line: line}, Ident: ident}
 }
 
-func (t *Template) newDiscard(pos Pos, line int) *DiscardNode {
-	return &DiscardNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeDiscard, Pos: pos, Line: line}}
+func (t *Template) newUnderscore(pos Pos, line int) *UnderscoreNode {
+	return &UnderscoreNode{NodeBase: NodeBase{TemplatePath: t.Name, NodeType: NodeUnderscore, Pos: pos, Line: line}}
 }
diff --git a/default.go b/default.go
index a805242..1bb3d01 100644
--- a/default.go
+++ b/default.go
@@ -51,8 +51,8 @@ func init() {
 		"array":     reflect.ValueOf(newSlice),
 		"isset": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
 			a.RequireNumOfArguments("isset", 1, -1)
-			for i := 0; i < len(a.argExpr); i++ {
-				if !a.runtime.isSet(a.argExpr[i]) {
+			for i := 0; i < a.NumOfArguments(); i++ {
+				if !a.IsSet(i) {
 					return valueBoolFALSE
 				}
 			}
diff --git a/docs/syntax.md b/docs/syntax.md
index 7ea8439..da51061 100644
--- a/docs/syntax.md
+++ b/docs/syntax.md
@@ -23,6 +23,7 @@
   - [Function calls](#function-calls)
     - [Prefix syntax](#prefix-syntax)
     - [Pipelining](#pipelining)
+    - [Piped argument slot](#piped-argument-slot)
 - [Control Structures](#control-structures)
   - [if](#if)
     - [if / else](#if--else)
@@ -252,6 +253,21 @@ Please note that the raw, unsafe, safeHtml or safeJs built-in escapers (or custo
     {{ raw: "hello" }}          <!-- valid -->
     {{ raw: "hello" | upper }}  <!-- invalid (upper would be evaluated after raw) -->
 
+#### Piped argument slot
+
+When pipelining, it can be desirable to use the piped value in a different slot in the function call than the first. To tell Jet where to inject the piped value, use `_`:
+
+    {{ 2 | repeat("foo", _) }}                 <!-- 'foo' two times -->
+    {{ 2 | repeat("foo", _) | repeat(_, 3) }}  <!-- 'foo' six times -->
+
+All of the following produce the same output as the second line in the example above:
+
+    {{ 2 | repeat("foo", _) | repeat(3) }}
+    {{ 2 | repeat: "foo", _ | repeat: 3 }}
+    {{ 2 | repeat: "foo", _ | repeat: _, 3 }}
+
+This feature is inspired by [function capturing](https://gleam.run/tour/functions.html#function-capturing) in Gleam.
+
 ## Control Structures
 
 ### if
diff --git a/eval.go b/eval.go
index c42d1f1..aea2687 100644
--- a/eval.go
+++ b/eval.go
@@ -284,10 +284,10 @@ RESTART:
 func (st *Runtime) executeSetList(set *SetNode) {
 	if set.IndexExprGetLookup {
 		value := st.evalPrimaryExpressionGroup(set.Right[0])
-		if set.Left[0].Type() != NodeDiscard {
+		if set.Left[0].Type() != NodeUnderscore {
 			st.executeSet(set.Left[0], value)
 		}
-		if set.Left[1].Type() != NodeDiscard {
+		if set.Left[1].Type() != NodeUnderscore {
 			if value.IsValid() {
 				st.executeSet(set.Left[1], valueBoolTRUE)
 			} else {
@@ -297,7 +297,7 @@ func (st *Runtime) executeSetList(set *SetNode) {
 	} else {
 		for i := 0; i < len(set.Left); i++ {
 			value := st.evalPrimaryExpressionGroup(set.Right[i])
-			if set.Left[i].Type() != NodeDiscard {
+			if set.Left[i].Type() != NodeUnderscore {
 				st.executeSet(set.Left[i], value)
 			}
 		}
@@ -307,10 +307,10 @@ func (st *Runtime) executeSetList(set *SetNode) {
 func (st *Runtime) executeLetList(set *SetNode) {
 	if set.IndexExprGetLookup {
 		value := st.evalPrimaryExpressionGroup(set.Right[0])
-		if set.Left[0].Type() != NodeDiscard {
+		if set.Left[0].Type() != NodeUnderscore {
 			st.variables[set.Left[0].(*IdentifierNode).Ident] = value
 		}
-		if set.Left[1].Type() != NodeDiscard {
+		if set.Left[1].Type() != NodeUnderscore {
 			if value.IsValid() {
 				st.variables[set.Left[1].(*IdentifierNode).Ident] = valueBoolTRUE
 			} else {
@@ -319,7 +319,7 @@ func (st *Runtime) executeLetList(set *SetNode) {
 		}
 	} else {
 		for i := 0; i < len(set.Left); i++ {
-			if set.Left[i].Type() != NodeDiscard {
+			if set.Left[i].Type() != NodeUnderscore {
 				st.variables[set.Left[i].(*IdentifierNode).Ident] = st.evalPrimaryExpressionGroup(set.Right[i])
 			}
 		}
@@ -656,7 +656,11 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value {
 		if baseExpr.Kind() != reflect.Func {
 			node.errorf("node %q is not func kind %q", node.BaseExpr, baseExpr.Type())
 		}
-		return st.evalCallExpression(baseExpr, node.Args)
+		ret, err := st.evalCallExpression(baseExpr, node.CallArgs)
+		if err != nil {
+			node.error(err)
+		}
+		return ret
 	case NodeIndexExpr:
 		node := node.(*IndexExprNode)
 		base := st.evalPrimaryExpressionGroup(node.Base)
@@ -1140,39 +1144,43 @@ func (st *Runtime) evalBaseExpressionGroup(node Node) reflect.Value {
 	return reflect.Value{}
 }
 
-func (st *Runtime) evalCallExpression(baseExpr reflect.Value, args []Expression, values ...reflect.Value) reflect.Value {
+func (st *Runtime) evalCallExpression(baseExpr reflect.Value, args CallArgs) (reflect.Value, error) {
+	return st.evalPipeCallExpression(baseExpr, args, nil)
+}
 
+func (st *Runtime) evalPipeCallExpression(baseExpr reflect.Value, args CallArgs, pipedArg *reflect.Value) (reflect.Value, error) {
 	if funcType.AssignableTo(baseExpr.Type()) {
-		return baseExpr.Interface().(Func)(Arguments{runtime: st, argExpr: args, argVal: values})
+		return baseExpr.Interface().(Func)(Arguments{runtime: st, args: args, pipedVal: pipedArg}), nil
 	}
 
-	i := len(args) + len(values)
-	var returns []reflect.Value
-	if i <= 10 {
-		returns = reflect_Call10(i, st, baseExpr, args, values...)
-	} else {
-		returns = reflect_Call(make([]reflect.Value, i, i), st, baseExpr, args, values...)
+	argValues, err := st.evaluateArgs(baseExpr.Type(), args, pipedArg)
+	if err != nil {
+		return reflect.Value{}, fmt.Errorf("call expression: %v", err)
 	}
 
+	var returns = baseExpr.Call(argValues)
 	if len(returns) == 0 {
-		return reflect.Value{}
+		return reflect.Value{}, nil
 	}
 
-	return returns[0]
+	return returns[0], nil
 }
 
 func (st *Runtime) evalCommandExpression(node *CommandNode) (reflect.Value, bool) {
 	term := st.evalPrimaryExpressionGroup(node.BaseExpr)
-	if node.Args != nil {
+	if node.Exprs != nil {
 		if term.Kind() == reflect.Func {
 			if term.Type() == safeWriterType {
 				st.evalSafeWriter(term, node)
 				return reflect.Value{}, true
 			}
-			return st.evalCallExpression(term, node.Args), false
-		} else {
-			node.Args[0].errorf("command %q type %s is not func", node.Args[0], term.Type())
+			ret, err := st.evalCallExpression(term, node.CallArgs)
+			if err != nil {
+				node.BaseExpr.error(err)
+			}
+			return ret, false
 		}
+		node.Exprs[0].errorf("command %q has arguments but is %s, not a function", node.Exprs[0], term.Type())
 	}
 	return term, false
 }
@@ -1209,28 +1217,31 @@ func (w *escapeWriter) Write(b []byte) (int, error) {
 }
 
 func (st *Runtime) evalSafeWriter(term reflect.Value, node *CommandNode, v ...reflect.Value) {
-
 	sw := &escapeWriter{rawWriter: st.Writer, safeWriter: term.Interface().(SafeWriter)}
 	for i := 0; i < len(v); i++ {
 		fastprinter.PrintValue(sw, v[i])
 	}
-	for i := 0; i < len(node.Args); i++ {
-		fastprinter.PrintValue(sw, st.evalPrimaryExpressionGroup(node.Args[i]))
+	for i := 0; i < len(node.Exprs); i++ {
+		fastprinter.PrintValue(sw, st.evalPrimaryExpressionGroup(node.Exprs[i]))
 	}
 }
 
 func (st *Runtime) evalCommandPipeExpression(node *CommandNode, value reflect.Value) (reflect.Value, bool) {
 	term := st.evalPrimaryExpressionGroup(node.BaseExpr)
-	if term.Kind() == reflect.Func {
-		if term.Type() == safeWriterType {
-			st.evalSafeWriter(term, node, value)
-			return reflect.Value{}, true
-		}
-		return st.evalCallExpression(term, node.Args, value), false
-	} else {
-		node.BaseExpr.errorf("pipe command %q type %s is not func", node.BaseExpr, term.Type())
+	if term.Kind() != reflect.Func {
+		node.BaseExpr.errorf("pipe command %q must be a function, but is %s", node.BaseExpr, term.Type())
 	}
-	return term, false
+
+	if term.Type() == safeWriterType {
+		st.evalSafeWriter(term, node, value)
+		return reflect.Value{}, true
+	}
+
+	ret, err := st.evalPipeCallExpression(term, node.CallArgs, &value)
+	if err != nil {
+		node.BaseExpr.error(err)
+	}
+	return ret, false
 }
 
 func (st *Runtime) evalPipelineExpression(node *PipeNode) (value reflect.Value, safeWriter bool) {
@@ -1244,61 +1255,71 @@ func (st *Runtime) evalPipelineExpression(node *PipeNode) (value reflect.Value,
 	return
 }
 
-func reflect_Call(arguments []reflect.Value, st *Runtime, fn reflect.Value, args []Expression, values ...reflect.Value) []reflect.Value {
-	typ := fn.Type()
-	numIn := typ.NumIn()
+var argValues = [24]reflect.Value{}
 
-	isVariadic := typ.IsVariadic()
-	if isVariadic {
-		numIn--
+func (st *Runtime) evaluateArgs(fnType reflect.Type, args CallArgs, pipedArg *reflect.Value) ([]reflect.Value, error) {
+	numArgs := len(args.Exprs)
+	if !args.HasPipeSlot && pipedArg != nil {
+		numArgs++
 	}
-	i, j := 0, 0
-
-	for ; i < numIn && i < len(values); i++ {
-		in := typ.In(i)
-		term := values[i]
-		if !term.Type().AssignableTo(in) {
-			term = term.Convert(in)
+	numArgsRequired := fnType.NumIn()
+	isVariadic := fnType.IsVariadic()
+	if isVariadic {
+		numArgsRequired--
+		if numArgs < numArgsRequired {
+			return nil, fmt.Errorf("need at least %d arguments, but have %d", numArgsRequired, numArgs)
+		}
+	} else {
+		if numArgs != numArgsRequired {
+			return nil, fmt.Errorf("need %d arguments, but have %d", numArgsRequired, numArgs)
 		}
-		arguments[i] = term
 	}
 
-	if isVariadic {
-		in := typ.In(numIn).Elem()
-		for ; i < len(values); i++ {
-			term := values[i]
-			if !term.Type().AssignableTo(in) {
-				term = term.Convert(in)
-			}
-			arguments[i] = term
+	slot, i := 0, 0
+	var term reflect.Value
+
+	if !args.HasPipeSlot && pipedArg != nil {
+		in := fnType.In(slot)
+		if !(*pipedArg).Type().AssignableTo(in) {
+			*pipedArg = (*pipedArg).Convert(in)
 		}
+		argValues[slot] = *pipedArg
+		slot++
 	}
 
-	for ; i < numIn && j < len(args); i, j = i+1, j+1 {
-		in := typ.In(i)
-		term := st.evalPrimaryExpressionGroup(args[j])
+	for i < len(args.Exprs) {
+		in := fnType.In(slot)
+		if args.Exprs[i].Type() == NodeUnderscore {
+			term = *pipedArg
+		} else {
+			term = st.evalPrimaryExpressionGroup(args.Exprs[i])
+		}
 		if !term.Type().AssignableTo(in) {
 			term = term.Convert(in)
 		}
-		arguments[i] = term
+		argValues[slot] = term
+		i++
+		slot++
 	}
 
 	if isVariadic {
-		in := typ.In(numIn).Elem()
-		for ; j < len(args); i, j = i+1, j+1 {
-			term := st.evalPrimaryExpressionGroup(args[j])
+		in := fnType.In(numArgsRequired).Elem()
+		for i < len(args.Exprs) {
+			if args.Exprs[i].Type() == NodeUnderscore {
+				term = *pipedArg
+			} else {
+				term = st.evalPrimaryExpressionGroup(args.Exprs[i])
+			}
 			if !term.Type().AssignableTo(in) {
 				term = term.Convert(in)
 			}
-			arguments[i] = term
+			argValues[slot] = term
+			i++
+			slot++
 		}
 	}
-	return fn.Call(arguments[0:i])
-}
 
-func reflect_Call10(i int, st *Runtime, fn reflect.Value, args []Expression, values ...reflect.Value) []reflect.Value {
-	var arguments [10]reflect.Value
-	return reflect_Call(arguments[0:i], st, fn, args, values...)
+	return argValues[:slot], nil
 }
 
 func isUint(kind reflect.Kind) bool {
diff --git a/func.go b/func.go
index a4c4c81..f704ee1 100644
--- a/func.go
+++ b/func.go
@@ -22,23 +22,35 @@ import (
 
 // Arguments holds the arguments passed to jet.Func.
 type Arguments struct {
-	runtime *Runtime
-	argExpr []Expression
-	argVal  []reflect.Value
+	runtime  *Runtime
+	args     CallArgs
+	pipedVal *reflect.Value
 }
 
 // IsSet checks whether an argument is set or not. It behaves like the build-in isset function.
 func (a *Arguments) IsSet(argumentIndex int) bool {
-	return a.runtime.isSet(a.argExpr[argumentIndex])
+	if argumentIndex < len(a.args.Exprs) {
+		if a.args.Exprs[argumentIndex].Type() == NodeUnderscore {
+			return a.pipedVal != nil
+		}
+		return a.runtime.isSet(a.args.Exprs[argumentIndex])
+	}
+	if len(a.args.Exprs) == 0 && argumentIndex == 0 {
+		return a.pipedVal != nil
+	}
+	return false
 }
 
 // Get gets an argument by index.
 func (a *Arguments) Get(argumentIndex int) reflect.Value {
-	if argumentIndex < len(a.argVal) {
-		return a.argVal[argumentIndex]
+	if argumentIndex < len(a.args.Exprs) {
+		if a.args.Exprs[argumentIndex].Type() == NodeUnderscore {
+			return *a.pipedVal
+		}
+		return a.runtime.evalPrimaryExpressionGroup(a.args.Exprs[argumentIndex])
 	}
-	if argumentIndex < len(a.argVal)+len(a.argExpr) {
-		return a.runtime.evalPrimaryExpressionGroup(a.argExpr[argumentIndex-len(a.argVal)])
+	if len(a.args.Exprs) == 0 && argumentIndex == 0 {
+		return *a.pipedVal
 	}
 	return reflect.Value{}
 }
@@ -51,7 +63,7 @@ func (a *Arguments) Panicf(format string, v ...interface{}) {
 // RequireNumOfArguments panics if the number of arguments is not in the range specified by min and max.
 // In case there is no minimum pass -1, in case there is no maximum pass -1 respectively.
 func (a *Arguments) RequireNumOfArguments(funcname string, min, max int) {
-	num := len(a.argExpr) + len(a.argVal)
+	num := a.NumOfArguments()
 	if min >= 0 && num < min {
 		a.Panicf("unexpected number of arguments in a call to %s", funcname)
 	} else if max >= 0 && num > max {
@@ -61,7 +73,14 @@ func (a *Arguments) RequireNumOfArguments(funcname string, min, max int) {
 
 // NumOfArguments returns the number of arguments
 func (a *Arguments) NumOfArguments() int {
-	return len(a.argExpr) + len(a.argVal)
+	num := len(a.args.Exprs)
+	if a.args.HasPipeSlot {
+		return num
+	}
+	if a.pipedVal != nil {
+		return num + 1
+	}
+	return num
 }
 
 // Runtime get the Runtime context
diff --git a/node.go b/node.go
index b0c53ac..d1c90f1 100644
--- a/node.go
+++ b/node.go
@@ -77,7 +77,7 @@ const (
 	NodeCommand                    //An element of a pipeline.
 	NodeField                      //A field or method name.
 	NodeIdentifier                 //An identifier; always a function name.
-	NodeDiscard                    //An underscore
+	NodeUnderscore                 //An underscore (discard in assignment, or slot in argument list for piped value)
 	NodeList                       //A list of Nodes.
 	NodePipe                       //A pipeline of commands.
 	NodeSet
@@ -188,16 +188,16 @@ type CommandNode struct {
 }
 
 func (c *CommandNode) append(arg Node) {
-	c.Args = append(c.Args, arg)
+	c.Exprs = append(c.Exprs, arg)
 }
 
 func (c *CommandNode) String() string {
-	if c.Args == nil {
+	if c.Exprs == nil {
 		return c.BaseExpr.String()
 	}
 
 	arguments := ""
-	for i, expr := range c.Args {
+	for i, expr := range c.Exprs {
 		if i > 0 {
 			arguments += ", "
 		}
@@ -216,12 +216,14 @@ func (i *IdentifierNode) String() string {
 	return i.Ident
 }
 
-// DiscardNode signals to discard the corresponding right side of an assignment.
-type DiscardNode struct {
+// UnderscoreNode is used for one of two things:
+// - signals to discard the corresponding right side of an assignment
+// - tells Jet where in a pipelined function call to inject the piped value
+type UnderscoreNode struct {
 	NodeBase
 }
 
-func (i *DiscardNode) String() string {
+func (i *UnderscoreNode) String() string {
 	return "_"
 }
 
@@ -614,17 +616,22 @@ func (s *NotExprNode) String() string {
 	return fmt.Sprintf("!%s", s.Expr)
 }
 
+type CallArgs struct {
+	Exprs       []Expression
+	HasPipeSlot bool
+}
+
 // CallExprNode represents a call expression
 // ex: expression '(' (expression (',' expression)* )? ')'
 type CallExprNode struct {
 	NodeBase
 	BaseExpr Expression
-	Args     []Expression
+	CallArgs
 }
 
 func (s *CallExprNode) String() string {
 	arguments := ""
-	for i, expr := range s.Args {
+	for i, expr := range s.Exprs {
 		if i > 0 {
 			arguments += ", "
 		}
diff --git a/parse.go b/parse.go
index 86a93e7..572b315 100644
--- a/parse.go
+++ b/parse.go
@@ -619,7 +619,7 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
 	leftloop:
 		for {
 			switch operand.Type() {
-			case NodeField, NodeChain, NodeIdentifier, NodeDiscard:
+			case NodeField, NodeChain, NodeIdentifier, NodeUnderscore:
 				left = append(left, operand)
 			default:
 				t.errorf("unexpected node in assign")
@@ -638,7 +638,7 @@ func (t *Template) assignmentOrExpression(context string) (operand Expression) {
 
 		if isLet {
 			for _, operand := range left {
-				if operand.Type() != NodeIdentifier && operand.Type() != NodeDiscard {
+				if operand.Type() != NodeIdentifier && operand.Type() != NodeUnderscore {
 					t.errorf("unexpected node type %s in variable declaration", operand)
 				}
 			}
@@ -720,8 +720,7 @@ func (t *Template) command(baseExpr Expression) *CommandNode {
 
 	if baseExpr.Type() == NodeCallExpr {
 		call := baseExpr.(*CallExprNode)
-		cmd.BaseExpr = call.BaseExpr
-		cmd.Args = call.Args
+		cmd.CallExprNode = *call
 		return cmd
 	}
 
@@ -730,7 +729,7 @@ func (t *Template) command(baseExpr Expression) *CommandNode {
 	next := t.nextNonSpace()
 	switch next.typ {
 	case itemColon:
-		cmd.Args = t.parseArguments()
+		cmd.CallArgs = t.parseArguments()
 	default:
 		t.backup()
 	}
@@ -781,7 +780,7 @@ RESET:
 		switch t.nextNonSpace().typ {
 		case itemLeftParen:
 			callExpr := t.newCallExpr(node.Position(), t.lex.lineNumber(), node)
-			callExpr.Args = t.parseArguments()
+			callExpr.CallArgs = t.parseArguments()
 			t.expect(itemRightParen, "call expression", "closing parenthesis")
 			node = callExpr
 			goto RESET
@@ -820,13 +819,28 @@ RESET:
 	return node
 }
 
-func (t *Template) parseArguments() []Expression {
-	args := []Expression{}
+func (t *Template) parseArguments() (args CallArgs) {
 	context := "call expression argument list"
+	args.Exprs = []Expression{}
 loop:
-	for t.peekNonSpace().typ != itemRightParen {
-		expr, endtoken := t.parseExpression(context)
-		args = append(args, expr)
+	for {
+		peek := t.peekNonSpace()
+		if peek.typ == itemRightParen {
+			break
+		}
+		var (
+			expr     Expression
+			endtoken item
+		)
+		expr, endtoken = t.parseExpression(context)
+		if expr.Type() == NodeUnderscore {
+			// slot for piped argument
+			if args.HasPipeSlot {
+				t.errorf("found two pipe slot markers ('_') for the same function call")
+			}
+			args.HasPipeSlot = true
+		}
+		args.Exprs = append(args.Exprs, expr)
 		switch endtoken.typ {
 		case itemComma:
 			// continue with closing parens (allowed because of multiline syntax) or next arg
@@ -835,7 +849,7 @@ loop:
 			break loop
 		}
 	}
-	return args
+	return
 }
 
 func (t *Template) parseControl(allowElseIf bool, context string) (pos Pos, line int, set *SetNode, expression Expression, list, elseList *ListNode) {
@@ -975,7 +989,7 @@ func (t *Template) term() Node {
 	case itemIdentifier:
 		return t.newIdentifier(token.val, token.pos, t.lex.lineNumber())
 	case itemUnderscore:
-		return t.newDiscard(token.pos, t.lex.lineNumber())
+		return t.newUnderscore(token.pos, t.lex.lineNumber())
 	case itemNil:
 		return t.newNil(token.pos)
 	case itemField:
diff --git a/utils/visitor.go b/utils/visitor.go
index 69516b0..6c6d414 100644
--- a/utils/visitor.go
+++ b/utils/visitor.go
@@ -193,7 +193,7 @@ func (vc VisitorContext) visitLogicalExprNode(logicalExprNode *jet.LogicalExprNo
 
 func (vc VisitorContext) visitCallExprNode(callExprNode *jet.CallExprNode) {
 	vc.visitNode(callExprNode.BaseExpr)
-	for _, node := range callExprNode.Args {
+	for _, node := range callExprNode.Exprs {
 		vc.visitNode(node)
 	}
 }
@@ -221,7 +221,7 @@ func (vc VisitorContext) visitSliceExprNode(sliceExprNode *jet.SliceExprNode) {
 
 func (vc VisitorContext) visitCommandNode(commandNode *jet.CommandNode) {
 	vc.visitNode(commandNode.BaseExpr)
-	for _, node := range commandNode.Args {
+	for _, node := range commandNode.Exprs {
 		vc.visitNode(node)
 	}
 }


Test patch:
diff --git a/eval_test.go b/eval_test.go
index 74898b5..ec19d43 100644
--- a/eval_test.go
+++ b/eval_test.go
@@ -165,10 +165,14 @@ func TestEvalActionNode(t *testing.T) {
 	RunJetTest(t, nil, nil, "actionNode", `hello {{"world"}}`, `hello world`)
 	RunJetTest(t, data, nil, "actionNode_func", `hello {{lower: "WORLD"}}`, `hello world`)
 	RunJetTest(t, data, nil, "actionNode_funcPipe", `hello {{lower: "WORLD" |upper}}`, `hello WORLD`)
-	RunJetTest(t, data, nil, "actionNode_funcPipe_Asd", `{{ "foo" | repeat(2) }}`, `foofoo`)
-	RunJetTest(t, data, nil, "actionNode_funcPipe_parens", `hello {{lower ( "WORLD" ) |upper}}`, `hello WORLD`)
+	RunJetTest(t, data, nil, "actionNode_funcPipe_parens", `{{ "foo" | repeat(2) }}`, `foofoo`)
+	RunJetTest(t, data, nil, "actionNode_funcPipe_parens2", `hello {{lower ( "WORLD" ) |upper}}`, `hello WORLD`)
 	RunJetTest(t, data, nil, "actionNode_funcPipeArg", `hello {{lower: "WORLD-" |upper|repeat: 2}}`, `hello WORLD-WORLD-`)
 	RunJetTest(t, data, nil, "actionNode_funcPipeArg_parens", `hello {{lower ("WORLD-" ) |upper|repeat(2) }}`, `hello WORLD-WORLD-`)
+	RunJetTest(t, data, nil, "pipelining_func_cap", `{{ 2 | repeat("foo", _) }}`, `foofoo`)
+	RunJetTest(t, data, nil, "pipelining_func_cap2", `{{ 2 | repeat("foo", _) | upper }}`, `FOOFOO`)
+	RunJetTest(t, data, nil, "pipelining_func_cap3", `{{ 2 | repeat("foo", _) | upper | repeat(_, 3) }}`, `FOOFOOFOOFOOFOOFOO`)
+	RunJetTest(t, data, nil, "pipelining_func_cap4", `{{ 2 | repeat: "foo", _ | upper | repeat: _, 3 }}`, `FOOFOOFOOFOOFOOFOO`)
 	RunJetTest(t, data, nil, "actionNode_Field", `Oi {{ user.Name }}`, `Oi José Santos`)
 	RunJetTest(t, data, nil, "actionNode_Field2", `Oi {{ user.Name }}<{{ user.Email }}>`, `Oi José Santos<email@example.com>`)
 	RunJetTest(t, data, nil, "actionNode_Method", `Oi {{ user.Format: "%s<%s>" }}`, `Oi José Santos<email@example.com>`)