text/template: allow grouping of pipelines using parentheses

Based on work by Russ Cox. From his CL:

        This is generally useful but especially helpful when trying
        to use the built-in boolean operators.  It lets you write:

        {{if not (f 1)}} foo {{end}}
        {{if and (f 1) (g 2)}} bar {{end}}
        {{if or (f 1) (g 2)}} quux {{end}}

        instead of

        {{if f 1 | not}} foo {{end}}
        {{if f 1}}{{if g 2}} bar {{end}}{{end}}
        {{$do := 0}}{{if f 1}}{{$do := 1}}{{else if g 2}}{{$do := 1}}{{end}}{{if $do}} quux {{end}}

The result can be a bit LISPy but the benefit in expressiveness and readability
for such a small change justifies it.

I believe no changes are required to html/template.

Fixes #3276.

R=golang-dev, adg, rogpeppe, minux.ma
CC=golang-dev
https://golang.org/cl/6482056
diff --git a/src/pkg/text/template/doc.go b/src/pkg/text/template/doc.go
index 3e4c66a..224775c 100644
--- a/src/pkg/text/template/doc.go
+++ b/src/pkg/text/template/doc.go
@@ -148,6 +148,8 @@
 	  The result is the value of invoking the function, fun(). The return
 	  types and values behave as in methods. Functions and function
 	  names are described below.
+	- Parentheses may be used for grouping, as in
+		print (.F1 arg1) (.F2 arg2)
 
 Arguments may evaluate to any type; if they are pointers the implementation
 automatically indirects to the base type when required.
@@ -228,6 +230,8 @@
 	{{"output" | printf "%q"}}
 		A function call whose final argument comes from the previous
 		command.
+	{{printf "%q" (print "out" "put")}}
+		A parenthesized argument.
 	{{"put" | printf "%s%s" "out" | printf "%q"}}
 		A more elaborate call.
 	{{"output" | printf "%s" | printf "%q"}}
diff --git a/src/pkg/text/template/exec.go b/src/pkg/text/template/exec.go
index a041351..1739a86 100644
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -564,6 +564,8 @@
 		return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, zero), typ)
 	case *parse.VariableNode:
 		return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ)
+	case *parse.PipeNode:
+		return s.validateType(s.evalPipeline(dot, arg), typ)
 	}
 	switch typ.Kind() {
 	case reflect.Bool:
@@ -666,6 +668,8 @@
 		return reflect.ValueOf(n.Text)
 	case *parse.VariableNode:
 		return s.evalVariableNode(dot, n, nil, zero)
+	case *parse.PipeNode:
+		return s.evalPipeline(dot, n)
 	}
 	s.errorf("can't handle assignment of %s to empty interface argument", n)
 	panic("not reached")
diff --git a/src/pkg/text/template/exec_test.go b/src/pkg/text/template/exec_test.go
index 95e0592..7f60dca 100644
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -337,6 +337,9 @@
 	{"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true},
 	{"pipeline func", "-{{call .VariadicFunc `llo` | call .VariadicFunc `he` }}-", "-<he+<llo>>-", tVal, true},
 
+	// Parenthesized expressions
+	{"parens in pipeline", "{{printf `%d %d %d` (1) (2 | add 3) (add 4 (add 5 6))}}", "1 5 15", tVal, true},
+
 	// If.
 	{"if true", "{{if true}}TRUE{{end}}", "TRUE", tVal, true},
 	{"if false", "{{if false}}TRUE{{else}}FALSE{{end}}", "FALSE", tVal, true},
@@ -524,6 +527,14 @@
 	return "vfunc"
 }
 
+func add(args ...int) int {
+	sum := 0
+	for _, x := range args {
+		sum += x
+	}
+	return sum
+}
+
 func stringer(s fmt.Stringer) string {
 	return s.String()
 }
@@ -531,6 +542,7 @@
 func testExecute(execTests []execTest, template *Template, t *testing.T) {
 	b := new(bytes.Buffer)
 	funcs := FuncMap{
+		"add":      add,
 		"count":    count,
 		"dddArg":   dddArg,
 		"oneArg":   oneArg,
diff --git a/src/pkg/text/template/parse/lex.go b/src/pkg/text/template/parse/lex.go
index 98f12a8..c73f533 100644
--- a/src/pkg/text/template/parse/lex.go
+++ b/src/pkg/text/template/parse/lex.go
@@ -46,10 +46,12 @@
 	itemField      // alphanumeric identifier, starting with '.', possibly chained ('.x.y')
 	itemIdentifier // alphanumeric identifier
 	itemLeftDelim  // left action delimiter
+	itemLeftParen  // '(' inside action
 	itemNumber     // simple number, including imaginary
 	itemPipe       // pipe symbol
 	itemRawString  // raw quoted string (includes quotes)
 	itemRightDelim // right action delimiter
+	itemRightParen // ')' inside action
 	itemString     // quoted string (includes quotes)
 	itemText       // plain text
 	itemVariable   // variable starting with '$', such as '$' or  '$1' or '$hello'.
@@ -78,12 +80,15 @@
 	itemField:        "field",
 	itemIdentifier:   "identifier",
 	itemLeftDelim:    "left delim",
+	itemLeftParen:    "(",
 	itemNumber:       "number",
 	itemPipe:         "pipe",
 	itemRawString:    "raw string",
 	itemRightDelim:   "right delim",
+	itemRightParen:   ")",
 	itemString:       "string",
 	itemVariable:     "variable",
+
 	// keywords
 	itemDot:      ".",
 	itemDefine:   "define",
@@ -133,6 +138,7 @@
 	width      int       // width of last rune read from input.
 	lastPos    int       // position of most recent item returned by nextItem
 	items      chan item // channel of scanned items.
+	parenDepth int       // nesting depth of ( ) exprs
 }
 
 // next returns the next rune in the input.
@@ -269,6 +275,7 @@
 		return lexComment
 	}
 	l.emit(itemLeftDelim)
+	l.parenDepth = 0
 	return lexInsideAction
 }
 
@@ -297,7 +304,10 @@
 	// Spaces separate and are ignored.
 	// Pipe symbols separate and are emitted.
 	if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
-		return lexRightDelim
+		if l.parenDepth == 0 {
+			return lexRightDelim
+		}
+		return l.errorf("unclosed left paren")
 	}
 	switch r := l.next(); {
 	case r == eof || r == '\n':
@@ -334,6 +344,17 @@
 	case isAlphaNumeric(r):
 		l.backup()
 		return lexIdentifier
+	case r == '(':
+		l.emit(itemLeftParen)
+		l.parenDepth++
+		return lexInsideAction
+	case r == ')':
+		l.emit(itemRightParen)
+		l.parenDepth--
+		if l.parenDepth < 0 {
+			return l.errorf("unexpected right paren %#U", r)
+		}
+		return lexInsideAction
 	case r <= unicode.MaxASCII && unicode.IsPrint(r):
 		l.emit(itemChar)
 		return lexInsideAction
@@ -386,7 +407,7 @@
 		return true
 	}
 	switch r {
-	case eof, ',', '|', ':':
+	case eof, ',', '|', ':', ')', '(':
 		return true
 	}
 	// Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will
diff --git a/src/pkg/text/template/parse/lex_test.go b/src/pkg/text/template/parse/lex_test.go
index f38057d..5a4e8b6 100644
--- a/src/pkg/text/template/parse/lex_test.go
+++ b/src/pkg/text/template/parse/lex_test.go
@@ -43,6 +43,16 @@
 		tRight,
 		tEOF,
 	}},
+	{"parens", "{{((3))}}", []item{
+		tLeft,
+		{itemLeftParen, 0, "("},
+		{itemLeftParen, 0, "("},
+		{itemNumber, 0, "3"},
+		{itemRightParen, 0, ")"},
+		{itemRightParen, 0, ")"},
+		tRight,
+		tEOF,
+	}},
 	{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
 	{"for", `{{for }}`, []item{tLeft, tFor, tRight, tEOF}},
 	{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
@@ -189,6 +199,18 @@
 		tLeft,
 		{itemError, 0, `bad number syntax: "3k"`},
 	}},
+	{"unclosed paren", "{{(3}}", []item{
+		tLeft,
+		{itemLeftParen, 0, "("},
+		{itemNumber, 0, "3"},
+		{itemError, 0, `unclosed left paren`},
+	}},
+	{"extra right paren", "{{3)}}", []item{
+		tLeft,
+		{itemNumber, 0, "3"},
+		{itemRightParen, 0, ")"},
+		{itemError, 0, `unexpected right paren U+0029 ')'`},
+	}},
 
 	// Fixed bugs
 	// Many elements in an action blew the lookahead until
diff --git a/src/pkg/text/template/parse/node.go b/src/pkg/text/template/parse/node.go
index 8a779ce..e6d106f 100644
--- a/src/pkg/text/template/parse/node.go
+++ b/src/pkg/text/template/parse/node.go
@@ -209,6 +209,10 @@
 		if i > 0 {
 			s += " "
 		}
+		if arg, ok := arg.(*PipeNode); ok {
+			s += "(" + arg.String() + ")"
+			continue
+		}
 		s += arg.String()
 	}
 	return s
diff --git a/src/pkg/text/template/parse/parse.go b/src/pkg/text/template/parse/parse.go
index 7ddb6ff..6dc2f0fb 100644
--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -349,10 +349,13 @@
 	pipe = newPipeline(t.lex.lineNumber(), decl)
 	for {
 		switch token := t.next(); token.typ {
-		case itemRightDelim:
+		case itemRightDelim, itemRightParen:
 			if len(pipe.Cmds) == 0 {
 				t.errorf("missing value for %s", context)
 			}
+			if token.typ == itemRightParen {
+				t.backup()
+			}
 			return
 		case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier,
 			itemNumber, itemNil, itemRawString, itemString, itemVariable:
@@ -456,11 +459,17 @@
 Loop:
 	for {
 		switch token := t.next(); token.typ {
-		case itemRightDelim:
+		case itemRightDelim, itemRightParen:
 			t.backup()
 			break Loop
 		case itemPipe:
 			break Loop
+		case itemLeftParen:
+			p := t.pipeline("parenthesized expression")
+			if t.next().typ != itemRightParen {
+				t.errorf("missing right paren in parenthesized expression")
+			}
+			cmd.append(p)
 		case itemError:
 			t.errorf("%s", token.val)
 		case itemIdentifier:
diff --git a/src/pkg/text/template/parse/parse_test.go b/src/pkg/text/template/parse/parse_test.go
index d7bfc78..da0df20 100644
--- a/src/pkg/text/template/parse/parse_test.go
+++ b/src/pkg/text/template/parse/parse_test.go
@@ -185,6 +185,8 @@
 		`{{.X | .Y}}`},
 	{"pipeline with decl", "{{$x := .X|.Y}}", noError,
 		`{{$x := .X | .Y}}`},
+	{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
+		`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
 	{"simple if", "{{if .X}}hello{{end}}", noError,
 		`{{if .X}}"hello"{{end}}`},
 	{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,