Skip to content
This repository has been archived by the owner on Mar 5, 2023. It is now read-only.

Commit

Permalink
all: implement return action and execTemplate builtin func
Browse files Browse the repository at this point in the history
  • Loading branch information
jo3-l committed Jun 7, 2021
1 parent 57c7fde commit 197a74f
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 24 deletions.
11 changes: 11 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ data, defined in detail in the corresponding sections that follow.
The current iteration of the innermost {{range pipeline}} or {{while pipeline}}
loop is stopped, and the loop starts the next iteration.
{{return}}
Stop execution of the current template.
{{return pipeline}}
Stop execution of the current template and return the result of evaluating
the pipeline to the caller.
{{template "name"}}
The template with the specified name is executed with nil data.
Expand Down Expand Up @@ -365,6 +372,10 @@ Predefined global functions are named as follows.
its arguments in a form suitable for embedding in a URL query.
This function is unavailable in html/template, with a few
exceptions.
execTemplate
Executes the associated template with the given name using the
data provided, returning the return value of the template, otherwise
nil.
The boolean functions take any zero value to be false and a non-zero
value to be true.
Expand Down
94 changes: 83 additions & 11 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ func initMaxExecDepth() int {
if runtime.GOARCH == "wasm" {
return 1000
}
return 100000
return 2000
}

// state represents the state of an execution. It's not part of the
// template so that multiple executions of the same template
// can execute in parallel.
type state struct {
tmpl *Template
wr io.Writer
node parse.Node // current node, for errors
vars []variable // push-down stack of variable values.
depth int // the height of the stack of executing templates.
operations int
tmpl *Template
wr io.Writer
node parse.Node // current node, for errors
vars []variable // push-down stack of variable values.
depth int // the height of the stack of executing templates.
operations int
returnValue reflect.Value

parent *state
}
Expand Down Expand Up @@ -218,6 +219,7 @@ func (t *Template) execute(wr io.Writer, data interface{}) (err error) {
if t.Tree == nil || t.Root == nil {
state.errorf("%q is an incomplete or empty template", t.Name())
}

state.walk(value, t.Root)
return
}
Expand Down Expand Up @@ -254,6 +256,7 @@ const (
controlFlowNone controlFlowSignal = iota
controlFlowBreak
controlFlowContinue
controlFlowReturnValue
)

// Walk functions step through the major pieces of the template structure,
Expand All @@ -278,6 +281,10 @@ func (s *state) walk(dot reflect.Value, node parse.Node) controlFlowSignal {
}
case *parse.RangeNode:
return s.walkRange(dot, node)
s.walkRange(dot, node)
case *parse.ReturnNode:
s.returnValue = s.evalPipeline(dot, node.Pipe)
return controlFlowReturnValue
case *parse.WhileNode:
return s.walkWhile(dot, node)
case *parse.TemplateNode:
Expand Down Expand Up @@ -387,8 +394,11 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) controlFlowSign
break
}
for i := 0; i < val.Len(); i++ {
if signal := oneIteration(reflect.ValueOf(i), val.Index(i)); signal == controlFlowBreak {
switch oneIteration(reflect.ValueOf(i), val.Index(i)) {
case controlFlowBreak:
return controlFlowNone
case controlFlowReturnValue:
return controlFlowReturnValue
}
}
return controlFlowNone
Expand All @@ -397,8 +407,11 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) controlFlowSign
break
}
for _, key := range sortKeys(val.MapKeys()) {
if signal := oneIteration(key, val.MapIndex(key)); signal == controlFlowBreak {
switch oneIteration(key, val.MapIndex(key)) {
case controlFlowBreak:
return controlFlowNone
case controlFlowReturnValue:
return controlFlowReturnValue
}
}
return controlFlowNone
Expand All @@ -412,8 +425,11 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) controlFlowSign
if !ok {
break
}
if signal := oneIteration(reflect.ValueOf(i), elem); signal == controlFlowBreak {
switch oneIteration(reflect.ValueOf(i), elem) {
case controlFlowBreak:
return controlFlowNone
case controlFlowReturnValue:
return controlFlowReturnValue
}
}
if i == 0 {
Expand Down Expand Up @@ -454,8 +470,11 @@ func (s *state) walkWhile(dot reflect.Value, w *parse.WhileNode) controlFlowSign

signal := s.walk(dot, w.List)
s.pop(mark)
if signal == controlFlowBreak {
switch signal {
case controlFlowBreak:
return controlFlowNone
case controlFlowReturnValue:
return controlFlowReturnValue
}
}

Expand Down Expand Up @@ -485,6 +504,7 @@ func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
newState.tmpl.maxOps = s.tmpl.maxOps
// No dynamic scoping: template invocations inherit no variables.
newState.vars = []variable{{"$", dot}}

newState.walk(dot, tmpl.Root)
}

Expand Down Expand Up @@ -723,6 +743,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
}

var (
stringType = reflect.TypeOf("")
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
Expand All @@ -737,11 +758,13 @@ func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, a
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}

typ := fun.Type()
numIn := len(args)
if final != missingVal {
numIn++
}

numFixed := len(args)
if typ.IsVariadic() {
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
Expand All @@ -755,6 +778,12 @@ 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 execTemplate.
if fun == builtinExecTemplate {
return s.callExecTemplate(dot, node, args, final)
}

// Build the arg list.
argv := make([]reflect.Value, numIn)
// Args must be evaluated. Fixed args first.
Expand Down Expand Up @@ -798,6 +827,49 @@ func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, a
return v
}

func (s *state) callExecTemplate(dot reflect.Value, node parse.Node, args []parse.Node, final reflect.Value) reflect.Value {
s.at(node)
s.incrOPs(100)

argv := make([]reflect.Value, 0, 2)
if len(args) > 0 {
argv = append(argv, s.evalArg(dot, stringType, args[0]))
}
if len(args) > 1 {
argv = append(argv, s.evalArg(dot, reflectValueType, args[1]).Interface().(reflect.Value))
}
if final != missingVal && len(argv) < 2 {
argv = append(argv, final)
}

name := argv[0].String()

var newDot reflect.Value
if len(argv) > 1 {
newDot = argv[1]
}

tmpl := s.tmpl.tmpl[name]
if tmpl == nil {
s.errorf("template %q not defined", name)
}
if s.depth == maxExecDepth {
s.errorf("exceeded maximum template depth (%v)", maxExecDepth)
}

newState := *s
newState.parent = s
newState.depth++
newState.tmpl = tmpl
newState.tmpl.maxOps = s.tmpl.maxOps
newState.vars = []variable{{"$", newDot}}

if newState.walk(newDot, tmpl.Root) == controlFlowReturnValue {
return newState.returnValue
}
return reflect.Value{}
}

// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
func canBeNil(typ reflect.Type) bool {
switch typ.Kind() {
Expand Down
40 changes: 40 additions & 0 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,17 @@ var execTests = []execTest{
{"len of int", "{{len 3}}", "", tVal, false},
{"len of nothing", "{{len .Empty0}}", "", tVal, false},

// ExecTemplate.
{"no return value", `{{define "f"}}hi{{end}}{{execTemplate "f"}}`, "hi<no value>", tVal, true},
{"return value", `{{define "f"}}{{return 1}}{{end}}{{execTemplate "f"}}`, "1", tVal, true},
{"use return value", `{{define "f"}}{{return 1}}{{end}}{{add (execTemplate "f") 1}}`, "2", tVal, true},
{"pass data and return back", `{{define "add1"}}{{return add . 1}}{{end}}{{execTemplate "add1" 1}}`, "2", tVal, true},
{"pass and use data", `{{define "sayMessage"}}text/template said: {{.}}{{end}}{{$s := execTemplate "sayMessage" "hello world"}}`, "text/template said: hello world", tVal, true},
{"recursive factorial", `{{define "fac"}}{{if eq . 0}}{{return 1}}{{end}}{{return (mult . (execTemplate "fac" (add . -1)))}}{{end}}{{execTemplate "fac" 5}}`, "120", tVal, true},
{"pass name using pipeline", `{{define "hh"}}hi{{end}}{{$s := "hh" | execTemplate}}`, "hi", tVal, true},
{"pass data using pipeline", `{{define "vv"}}{{.}}{{end}}{{$s := 1 | execTemplate "vv"}}`, "1", tVal, true},
{"no args passed", `{{define "xx"}}{{end}}{{execTemplate}}`, "", tVal, false},

// With.
{"with true", "{{with true}}{{.}}{{end}}", "true", tVal, true},
{"with false", "{{with false}}{{.}}{{else}}FALSE{{end}}", "FALSE", tVal, true},
Expand Down Expand Up @@ -560,6 +571,13 @@ var execTests = []execTest{
{"while empty value with else", "{{while false}}NOTEXECUTED{{else}}ELSELIST{{end}}AFTER", "ELSELISTAFTER", tVal, true},
{"while infinite loop", "{{while true}}{{end}}", "", tVal, false},

// Return.
{"return top level", `12{{return}}23`, "12", tVal, true},
{"return in nested template", `{{define "tmpl"}}12{{return}}34{{end}}{{template "tmpl"}}45`, "1245", tVal, true},
{"return in range", `{{range .SI}}{{return}}23{{end}}34`, "", tVal, true},
{"return in if", `{{if true}}{{return}}{{end}}12`, "", tVal, true},
{"return with value", `12{{return 34}}45`, "12", tVal, true},

// Cute examples.
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
{"or as if false", `{{or .SIEmpty "slice is empty"}}`, "slice is empty", tVal, true},
Expand Down Expand Up @@ -708,6 +726,14 @@ func add(args ...int) int {
return sum
}

func mult(args ...int) int {
res := 1
for _, x := range args {
res *= x
}
return res
}

func echo(arg interface{}) interface{} {
return arg
}
Expand All @@ -734,6 +760,7 @@ func mapOfThree() interface{} {
func testExecute(execTests []execTest, template *Template, t *testing.T) {
b := new(bytes.Buffer)
funcs := FuncMap{
"mult": mult,
"add": add,
"count": count,
"dddArg": dddArg,
Expand Down Expand Up @@ -1448,6 +1475,19 @@ func TestMaxExecDepth(t *testing.T) {
}
}

func TestMaxExecTemplateDepth(t *testing.T) {
tmpl := Must(New("tmpl").Parse(`{{execTemplate "tmpl" .}}`))
err := tmpl.Execute(ioutil.Discard, nil)
got := "<nil>"
if err != nil {
got = err.Error()
}
const want = "exceeded maximum template depth"
if !strings.Contains(got, want) {
t.Errorf("got error %q; want %q", got, want)
}
}

func TestAddrOfIndex(t *testing.T) {
// golang.org/issue/14916.
// Before index worked on reflect.Values, the .String could not be
Expand Down
35 changes: 23 additions & 12 deletions funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,27 @@ import (
// type can return interface{} or reflect.Value.
type FuncMap map[string]interface{}

var builtinExecTemplate = reflect.ValueOf(execTemplate)

// builtins returns the FuncMap.
// It is not a global variable so the linker can dead code eliminate
// more when this isn't called. See golang.org/issue/36021.
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
func builtins() FuncMap {
return FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
"and": and,
"call": call,
"execTemplate": execTemplate,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,

// Comparisons
"eq": eq, // ==
Expand Down Expand Up @@ -250,6 +253,14 @@ func length(item interface{}) (int, error) {
return 0, fmt.Errorf("len of type %s", v.Type())
}

// Template invocation

// execTemplate executes the associated template with the given name and data
// and returns its return value.
func execTemplate(name string, data ...reflect.Value) reflect.Value {
panic("unreachable") // implemented as a special case in evalCall
}

// Function invocation

// call returns the result of evaluating the first argument as a function.
Expand Down
2 changes: 2 additions & 0 deletions parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const (
itemIf // if keyword
itemNil // the untyped nil constant, easiest to treat as a keyword
itemRange // range keyword
itemReturn // return keyword
itemTemplate // template keyword
itemWith // with keyword
itemWhile // while keyword
Expand All @@ -85,6 +86,7 @@ var key = map[string]itemType{
"end": itemEnd,
"if": itemIf,
"range": itemRange,
"return": itemReturn,
"nil": itemNil,
"template": itemTemplate,
"with": itemWith,
Expand Down
5 changes: 4 additions & 1 deletion parse/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var itemName = map[itemType]string{
itemEnd: "end",
itemNil: "nil",
itemRange: "range",
itemReturn: "return",
itemTemplate: "template",
itemWith: "with",
itemWhile: "while",
Expand Down Expand Up @@ -205,7 +206,7 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"keywords", "{{range if else end with break continue while}}", []item{
{"keywords", "{{range if else end with break continue while return}}", []item{
tLeft,
mkItem(itemRange, "range"),
tSpace,
Expand All @@ -222,6 +223,8 @@ var lexTests = []lexTest{
mkItem(itemContinue, "continue"),
tSpace,
mkItem(itemWhile, "while"),
tSpace,
mkItem(itemReturn, "return"),
tRight,
tEOF,
}},
Expand Down
Loading

0 comments on commit 197a74f

Please sign in to comment.