Skip to content

Commit

Permalink
all: implement while loop action
Browse files Browse the repository at this point in the history
  • Loading branch information
jo3-l committed Apr 16, 2021
1 parent 08367bc commit b174908
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 3 deletions.
8 changes: 8 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ data, defined in detail in the corresponding sections that follow.
T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed.
{{while pipeline}} T1 {{end}}
Execute T1 while the value of the pipeline is not empty. Dot is
unaffected.
{{while pipeline}} T1 {{else}} T0 {{end}}
Execute T1 while the value of the pipeline is not empty. If the initial
value of the pipeline was empty, evaluate T0. Dot is unaffected.
{{template "name"}}
The template with the specified name is executed with nil data.
Expand Down
32 changes: 32 additions & 0 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
}
case *parse.RangeNode:
s.walkRange(dot, node)
case *parse.WhileNode:
s.walkWhile(dot, node)
case *parse.TemplateNode:
s.walkTemplate(dot, node)
case *parse.TextNode:
Expand Down Expand Up @@ -400,6 +402,36 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
}
}

func (s *state) walkWhile(dot reflect.Value, w *parse.WhileNode) {
s.incrOPs(1)

s.at(w)
defer s.pop(s.mark())
// mark top of stack before any variables in the body are pushed.
mark := s.mark()

i := 0
for ; ; i++ {
s.incrOPs(5)

val, _ := indirect(s.evalPipeline(dot, w.Pipe))
truth, ok := isTrue(val)
if !ok {
s.errorf("while can't use %v", val)
}
if !truth {
break
}

s.walk(dot, w.List)
s.pop(mark)
}

if i == 0 && w.ElseList != nil {
s.walk(dot, w.ElseList)
}
}

func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
s.at(t)
s.incrOPs(100)
Expand Down
15 changes: 13 additions & 2 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,17 @@ var execTests = []execTest{
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},

// While.
{"while number", "{{$i := 0}}{{while lt $i 5}}<{{$i}}>{{$i = add $i 1}}{{end}}", "<0><1><2><3><4>", tVal, true},
{"while number access dot", "{{$i := 0}}{{while lt $i 5}}<{{.I}}>{{$i = add $i 1}}{{end}}", "<17><17><17><17><17>", tVal, true},
{"while declaration 01", "{{$i := 0}}{{while $b := lt $i 5}}{{$b}}{{$i = add $i 1}}{{end}}", "truetruetruetruetrue", tVal, true},
{"while declaration 02", "{{$i := 0}}{{$x := 7}}{{while lt $i 5}}{{$i = add $i 1}}{{$x := 5}}{{end}}{{$x}}", "7", tVal, true},
{"while declaration 03", "{{$i := 0}}{{$x := 7}}{{while lt $i 5}}{{$i = add $i 1}}{{$x = 5}}{{end}}{{$x}}", "5", tVal, true},
{"while falsey value", "{{while .MSIEmpty}}test{{end}}", "", tVal, true},
{"while falsey value else", "{{while .MSIEmpty}}test{{else}}falsey{{end}}", "falsey", tVal, true},
// should be stopped by MaxOps
{"while infinite loop", "{{while true}}{{end}}", "", tVal, false},

// 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 @@ -707,9 +718,9 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) {
var tmpl *Template
var err error
if template == nil {
tmpl, err = New(test.name).Funcs(funcs).Parse(test.input)
tmpl, err = New(test.name).Funcs(funcs).MaxOps(1_000_000).Parse(test.input)
} else {
tmpl, err = template.New(test.name).Funcs(funcs).Parse(test.input)
tmpl, err = template.New(test.name).Funcs(funcs).MaxOps(1_000_000).Parse(test.input)
}
if err != nil {
t.Errorf("%s: parse error: %s", test.name, err)
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 (
itemRange // range keyword
itemTemplate // template keyword
itemWith // with keyword
itemWhile // while keyword
)

var key = map[string]itemType{
Expand All @@ -83,6 +84,7 @@ var key = map[string]itemType{
"nil": itemNil,
"template": itemTemplate,
"with": itemWith,
"while": itemWhile,
}

const eof = -1
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{
itemRange: "range",
itemTemplate: "template",
itemWith: "with",
itemWhile: "while",
}

func (i itemType) String() string {
Expand Down Expand Up @@ -192,7 +193,7 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"keywords", "{{range if else end with}}", []item{
{"keywords", "{{range if else end with while}}", []item{
tLeft,
mkItem(itemRange, "range"),
tSpace,
Expand All @@ -203,6 +204,8 @@ var lexTests = []lexTest{
mkItem(itemEnd, "end"),
tSpace,
mkItem(itemWith, "with"),
tSpace,
mkItem(itemWhile, "while"),
tRight,
tEOF,
}},
Expand Down
18 changes: 18 additions & 0 deletions parse/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const (
NodeTemplate // A template invocation action.
NodeVariable // A $ variable.
NodeWith // A with action.
NodeWhile // A while action.
)

// Nodes.
Expand Down Expand Up @@ -746,6 +747,8 @@ func (b *BranchNode) String() string {
name = "range"
case NodeWith:
name = "with"
case NodeWhile:
name = "while"
default:
panic("unknown branch type")
}
Expand All @@ -767,6 +770,8 @@ func (b *BranchNode) Copy() Node {
return b.tr.newRange(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
case NodeWith:
return b.tr.newWith(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
case NodeWhile:
return b.tr.newWhile(b.Pos, b.Line, b.Pipe, b.List, b.ElseList)
default:
panic("unknown branch type")
}
Expand Down Expand Up @@ -811,6 +816,19 @@ func (w *WithNode) Copy() Node {
return w.tr.newWith(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
}

// WhileNode represents a {{while}} action and its commands.
type WhileNode struct {
BranchNode
}

func (t *Tree) newWhile(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *WhileNode {
return &WhileNode{BranchNode{tr: t, NodeType: NodeRange, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
}

func (w *WhileNode) Copy() Node {
return w.tr.newWhile(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
}

// TemplateNode represents a {{template}} action.
type TemplateNode struct {
NodeType
Expand Down
11 changes: 11 additions & 0 deletions parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ func IsEmptyTree(n Node) bool {
}
}
return true
case *WhileNode:
case *RangeNode:
case *TemplateNode:
case *TextNode:
Expand Down Expand Up @@ -373,6 +374,8 @@ func (t *Tree) action() (n Node) {
return t.templateControl()
case itemWith:
return t.withControl()
case itemWhile:
return t.whileControl()
}
t.backup()
token := t.peek()
Expand Down Expand Up @@ -514,6 +517,14 @@ func (t *Tree) withControl() Node {
return t.newWith(t.parseControl(false, "with"))
}

// While:
// {{while pipeline}} itemList {{end}}
// {{while pipeline}} itemList {{else}} itemList {{end}}
// While keyword is past.
func (t *Tree) whileControl() Node {
return t.newWhile(t.parseControl(false, "while"))
}

// End:
// {{end}}
// End keyword is past.
Expand Down
14 changes: 14 additions & 0 deletions parse/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ var parseTests = []parseTest{
`{{range $x := .SI}}{{.}}{{end}}`},
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
`{{range $x, $y := .SI}}{{.}}{{end}}`},
{"simple while", "{{while .X}}hello{{end}}", noError,
`{{while .X}}"hello"{{end}}`},
{"chained field while", "{{while .X.Y.Z}}hello{{end}}", noError,
`{{while .X.Y.Z}}"hello"{{end}}`},
{"nested while", "{{while .X}}hello{{while .Y}}goodbye{{end}}{{end}}", noError,
`{{while .X}}"hello"{{while .Y}}"goodbye"{{end}}{{end}}`},
{"while with else", "{{while .X}}true{{else}}false{{end}}", noError,
`{{while .X}}"true"{{else}}"false"{{end}}`},
{"while over pipeline", "{{while .X|.M}}true{{else}}false{{end}}", noError,
`{{while .X | .M}}"true"{{else}}"false"{{end}}`},
{"while boolean", "{{while .B}}{{.}}{{end}}", noError,
`{{while .B}}{{.}}{{end}}`},
{"while 1 var", "{{while $x := add .I 1}}hello{{end}}", noError,
`{{while $x := add .I 1}}"hello"{{end}}`},
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
{"template", "{{template `x`}}", noError,
Expand Down

0 comments on commit b174908

Please sign in to comment.