diff --git a/src/text/template/doc.go b/src/text/template/doc.go index 7b302943369..0ea132e8e64 100644 --- a/src/text/template/doc.go +++ b/src/text/template/doc.go @@ -307,9 +307,10 @@ Predefined global functions are named as follows. and Returns the boolean AND of its arguments by returning the - first empty argument or the last argument, that is, - "and x y" behaves as "if x then y else x". All the - arguments are evaluated. + first empty argument or the last argument. That is, + "and x y" behaves as "if x then y else x." + Evaluation proceeds through the arguments left to right + and returns when the result is determined. call Returns the result of calling the first argument, which must be a function, with the remaining arguments as parameters. @@ -344,8 +345,9 @@ Predefined global functions are named as follows. or Returns the boolean OR of its arguments by returning the first non-empty argument or the last argument, that is, - "or x y" behaves as "if x then x else y". All the - arguments are evaluated. + "or x y" behaves as "if x then x else y". + Evaluation proceeds through the arguments left to right + and returns when the result is determined. print An alias for fmt.Sprint printf diff --git a/src/text/template/exec.go b/src/text/template/exec.go index 5ad3b4ec582..6e005b57d77 100644 --- a/src/text/template/exec.go +++ b/src/text/template/exec.go @@ -572,11 +572,11 @@ func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ide func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value { s.at(node) name := node.Ident - function, ok := findFunction(name, s.tmpl) + function, isBuiltin, ok := findFunction(name, s.tmpl) if !ok { s.errorf("%q is not a defined function", name) } - return s.evalCall(dot, function, cmd, name, args, final) + return s.evalCall(dot, function, isBuiltin, cmd, name, args, final) } // evalField evaluates an expression like (.Field) or (.Field arg1 arg2). @@ -605,7 +605,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, ptr = ptr.Addr() } if method := ptr.MethodByName(fieldName); method.IsValid() { - return s.evalCall(dot, method, node, fieldName, args, final) + return s.evalCall(dot, method, false, node, fieldName, args, final) } hasArgs := len(args) > 1 || final != missingVal // It's not a method; must be a field of a struct or an element of a map. @@ -669,7 +669,7 @@ var ( // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] // as the function itself. -func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { +func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { if args != nil { args = args[1:] // Zeroth arg is function name/node; not passed to function. } @@ -691,6 +691,20 @@ func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, a // TODO: This could still be a confusing error; maybe goodFunc should provide info. s.errorf("can't call method/function %q with %d results", name, typ.NumOut()) } + + // Special case for builtin and/or, which short-circuit. + if isBuiltin && (name == "and" || name == "or") { + argType := typ.In(0) + var v reflect.Value + for _, arg := range args { + v = s.evalArg(dot, argType, arg).Interface().(reflect.Value) + if truth(v) == (name == "or") { + break + } + } + return v + } + // Build the arg list. argv := make([]reflect.Value, numIn) // Args must be evaluated. Fixed args first. diff --git a/src/text/template/exec_test.go b/src/text/template/exec_test.go index ef521645a7f..ae67b9334f3 100644 --- a/src/text/template/exec_test.go +++ b/src/text/template/exec_test.go @@ -481,6 +481,10 @@ var execTests = []execTest{ {"not", "{{not true}} {{not false}}", "false true", nil, true}, {"and", "{{and false 0}} {{and 1 0}} {{and 0 true}} {{and 1 1}}", "false 0 0 1", nil, true}, {"or", "{{or 0 0}} {{or 1 0}} {{or 0 true}} {{or 1 1}}", "0 1 true 1", nil, true}, + {"or short-circuit", "{{or 0 1 (die)}}", "1", nil, true}, + {"and short-circuit", "{{and 1 0 (die)}}", "0", nil, true}, + {"or short-circuit2", "{{or 0 0 (die)}}", "", nil, false}, + {"and short-circuit2", "{{and 1 1 (die)}}", "", nil, false}, {"boolean if", "{{if and true 1 `hi`}}TRUE{{else}}FALSE{{end}}", "TRUE", tVal, true}, {"boolean if not", "{{if and true 1 `hi` | not}}TRUE{{else}}FALSE{{end}}", "FALSE", nil, true}, @@ -764,6 +768,7 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) { "add": add, "count": count, "dddArg": dddArg, + "die": func() bool { panic("die") }, "echo": echo, "makemap": makemap, "mapOfThree": mapOfThree, diff --git a/src/text/template/funcs.go b/src/text/template/funcs.go index fff833ed298..11e2e903c85 100644 --- a/src/text/template/funcs.go +++ b/src/text/template/funcs.go @@ -139,18 +139,18 @@ func goodName(name string) bool { } // findFunction looks for a function in the template, and global map. -func findFunction(name string, tmpl *Template) (reflect.Value, bool) { +func findFunction(name string, tmpl *Template) (v reflect.Value, isBuiltin, ok bool) { if tmpl != nil && tmpl.common != nil { tmpl.muFuncs.RLock() defer tmpl.muFuncs.RUnlock() if fn := tmpl.execFuncs[name]; fn.IsValid() { - return fn, true + return fn, false, true } } if fn := builtinFuncs()[name]; fn.IsValid() { - return fn, true + return fn, true, true } - return reflect.Value{}, false + return reflect.Value{}, false, false } // prepareArg checks if value can be used as an argument of type argType, and @@ -382,31 +382,13 @@ func truth(arg reflect.Value) bool { // and computes the Boolean AND of its arguments, returning // the first false argument it encounters, or the last argument. func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value { - if !truth(arg0) { - return arg0 - } - for i := range args { - arg0 = args[i] - if !truth(arg0) { - break - } - } - return arg0 + panic("unreachable") // implemented as a special case in evalCall } // or computes the Boolean OR of its arguments, returning // the first true argument it encounters, or the last argument. func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value { - if truth(arg0) { - return arg0 - } - for i := range args { - arg0 = args[i] - if truth(arg0) { - break - } - } - return arg0 + panic("unreachable") // implemented as a special case in evalCall } // not returns the Boolean negation of its argument.