Skip to content

Commit

Permalink
all: add break & continue loop actions
Browse files Browse the repository at this point in the history
This is mostly taken from https://go-review.googlesource.com/c/go/+/66410/, with some edits to support while actions. The above commit was reverted due to issues with html/template (see golang/go#23683). However, as we don't have such issues, there should be no issue adding it to this fork.
  • Loading branch information
jo3-l committed Apr 16, 2021
1 parent b174908 commit d1f133b
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 24 deletions.
6 changes: 6 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ data, defined in detail in the corresponding sections that follow.
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.
{{break}}
Break out of the surrounding range or while loop.
{{continue}}
Begin the next iteration of the surrounding range or while loop.
{{template "name"}}
The template with the specified name is executed with nil data.
Expand Down
90 changes: 67 additions & 23 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type state struct {
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.
loopDepth int // nesting level of range/while loops.
operations int

parent *state
Expand Down Expand Up @@ -247,9 +248,17 @@ func (t *Template) DefinedTemplates() string {
return s
}

type loopControl int8

const (
loopNone loopControl = iota // no action.
loopBreak // break out of loop.
loopContinue // continues next loop iteration.
)

// Walk functions step through the major pieces of the template structure,
// generating output as they go.
func (s *state) walk(dot reflect.Value, node parse.Node) {
func (s *state) walk(dot reflect.Value, node parse.Node) loopControl {
s.at(node)
switch node := node.(type) {
case *parse.ActionNode:
Expand All @@ -260,31 +269,44 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
s.printValue(node, val)
}
case *parse.IfNode:
s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
return s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
case *parse.ListNode:
for _, node := range node.Nodes {
s.walk(dot, node)
if c := s.walk(dot, node); c != loopNone {
return c
}
}
case *parse.RangeNode:
s.walkRange(dot, node)
return s.walkRange(dot, node)
case *parse.WhileNode:
s.walkWhile(dot, node)
return s.walkWhile(dot, node)
case *parse.TemplateNode:
s.walkTemplate(dot, node)
case *parse.TextNode:
if _, err := s.wr.Write(node.Text); err != nil {
s.writeError(err)
}
case *parse.WithNode:
s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
return s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
case *parse.BreakNode:
if s.loopDepth == 0 {
s.errorf("invalid break outside of loop")
}
return loopBreak
case *parse.ContinueNode:
if s.loopDepth == 0 {
s.errorf("invalid continue outside of loop")
}
return loopContinue
default:
s.errorf("unknown node: %s", node)
}
return loopNone
}

// walkIfOrWith walks an 'if' or 'with' node. The two control structures
// are identical in behavior except that 'with' sets dot.
func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) {
func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) loopControl {
defer s.pop(s.mark())
val := s.evalPipeline(dot, pipe)
truth, ok := isTrue(val)
Expand All @@ -293,13 +315,15 @@ func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.
}
if truth {
if typ == parse.NodeWith {
s.walk(val, list)
return s.walk(val, list)
} else {
s.walk(dot, list)
return s.walk(dot, list)
}
} else if elseList != nil {
s.walk(dot, elseList)
return s.walk(dot, elseList)
}

return loopNone
}

// IsTrue reports whether the value is 'true', in the sense of not the zero of its type,
Expand Down Expand Up @@ -337,15 +361,16 @@ func isTrue(val reflect.Value) (truth, ok bool) {
return truth, true
}

func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) loopControl {
s.incrOPs(1)

s.at(r)
defer s.pop(s.mark())
val, _ := indirect(s.evalPipeline(dot, r.Pipe))
// mark top of stack before any variables in the body are pushed.
mark := s.mark()
oneIteration := func(index, elem reflect.Value) {
s.loopDepth++
oneIteration := func(index, elem reflect.Value) loopControl {
s.incrOPs(1)

// Set top var (lexically the second if there are two) to the element.
Expand All @@ -356,26 +381,33 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
if len(r.Pipe.Decl) > 1 {
s.setTopVar(2, index)
}
s.walk(elem, r.List)
ctrl := s.walk(elem, r.List)
s.pop(mark)
return ctrl
}
switch val.Kind() {
case reflect.Array, reflect.Slice:
if val.Len() == 0 {
break
}
for i := 0; i < val.Len(); i++ {
oneIteration(reflect.ValueOf(i), val.Index(i))
if ctrl := oneIteration(reflect.ValueOf(i), val.Index(i)); ctrl == loopBreak {
break
}
}
return
s.loopDepth--
return loopNone
case reflect.Map:
if val.Len() == 0 {
break
}
for _, key := range sortKeys(val.MapKeys()) {
oneIteration(key, val.MapIndex(key))
if ctrl := oneIteration(key, val.MapIndex(key)); ctrl == loopBreak {
break
}
}
return
s.loopDepth--
return loopNone
case reflect.Chan:
if val.IsNil() {
break
Expand All @@ -386,29 +418,35 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
if !ok {
break
}
oneIteration(reflect.ValueOf(i), elem)
if ctrl := oneIteration(reflect.ValueOf(i), elem); ctrl == loopBreak {
break
}
}
if i == 0 {
break
}
return
s.loopDepth--
return loopNone
case reflect.Invalid:
break // An invalid value is likely a nil map, etc. and acts like an empty map.
default:
s.errorf("range can't iterate over %v", val)
}
s.loopDepth--
if r.ElseList != nil {
s.walk(dot, r.ElseList)
return s.walk(dot, r.ElseList)
}
return loopNone
}

func (s *state) walkWhile(dot reflect.Value, w *parse.WhileNode) {
func (s *state) walkWhile(dot reflect.Value, w *parse.WhileNode) loopControl {
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()
s.loopDepth++

i := 0
for ; ; i++ {
Expand All @@ -423,13 +461,19 @@ func (s *state) walkWhile(dot reflect.Value, w *parse.WhileNode) {
break
}

s.walk(dot, w.List)
ctrl := s.walk(dot, w.List)
s.pop(mark)
if ctrl == loopBreak {
break
}
}

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

return loopNone
}

func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
Expand Down
8 changes: 8 additions & 0 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ var execTests = []execTest{
{"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
{"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},
{"range quick break", `{{range .SI}}{{break}}{{.}}{{end}}`, "", tVal, true},
{"range break after two", `{{range $i, $x := .SI}}{{if ge $i 2}}{{break}}{{end}}{{.}}{{end}}`, "34", tVal, true},
{"range continue", `{{range .SI}}{{continue}}{{.}}{{end}}`, "", tVal, true},
{"range continue condition", `{{range .SI}}{{if eq . 3 }}{{continue}}{{end}}{{.}}{{end}}`, "45", tVal, true},

// While.
{"while number", "{{$i := 0}}{{while lt $i 5}}<{{$i}}>{{$i = add $i 1}}{{end}}", "<0><1><2><3><4>", tVal, true},
Expand All @@ -543,6 +547,10 @@ var execTests = []execTest{
{"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},
{"while quick break", "{{while true}}{{break}}1{{end}}", "", tVal, true},
{"while break at three", "{{$i := 0}}{{while lt $i 5}}{{if eq $i 3}}{{break}}{{end}}<{{$i}}>{{$i = add $i 1}}{{end}}", "<0><1><2>", tVal, true},
{"while range break", "{{$i := 0}}{{while lt $i 5}}<{{$i}}>{{range .SI}}{{break}}{{end}}{{$i = add $i 1}}{{end}}", "<0><1><2><3><4>", tVal, true},
{"while continue", "{{$i := 0}}{{while lt $i 5}}{{$i = add $i 1}}{{continue}}<{{$i}}>{{end}}", "", tVal, true},
// should be stopped by MaxOps
{"while infinite loop", "{{while true}}{{end}}", "", tVal, false},

Expand Down
4 changes: 4 additions & 0 deletions parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const (
// Keywords appear after all the rest.
itemKeyword // used only to delimit the keywords
itemBlock // block keyword
itemBreak // break keyword
itemContinue // continue keyword
itemDot // the cursor, spelled '.'
itemDefine // define keyword
itemElse // else keyword
Expand All @@ -76,6 +78,8 @@ const (
var key = map[string]itemType{
".": itemDot,
"block": itemBlock,
"break": itemBreak,
"continue": itemContinue,
"define": itemDefine,
"else": itemElse,
"end": itemEnd,
Expand Down
8 changes: 7 additions & 1 deletion parse/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ var itemName = map[itemType]string{

// keywords
itemDot: ".",
itemBreak: "break",
itemBlock: "block",
itemContinue: "continue",
itemDefine: "define",
itemElse: "else",
itemIf: "if",
Expand Down Expand Up @@ -193,7 +195,7 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"keywords", "{{range if else end with while}}", []item{
{"keywords", "{{range if else end with while break continue}}", []item{
tLeft,
mkItem(itemRange, "range"),
tSpace,
Expand All @@ -205,6 +207,10 @@ var lexTests = []lexTest{
tSpace,
mkItem(itemWith, "with"),
tSpace,
mkItem(itemBreak, "break"),
tSpace,
mkItem(itemContinue, "continue"),
tSpace,
mkItem(itemWhile, "while"),
tRight,
tEOF,
Expand Down
48 changes: 48 additions & 0 deletions parse/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ const (
NodeText NodeType = iota // Plain text.
NodeAction // A non-control action such as a field evaluation.
NodeBool // A boolean constant.
NodeBreak // A break action.
NodeChain // A sequence of field accesses.
NodeCommand // An element of a pipeline.
NodeContinue // A continue action.
NodeDot // The cursor, dot.
nodeElse // An else action. Not added to tree.
nodeEnd // An end action. Not added to tree.
Expand Down Expand Up @@ -829,6 +831,52 @@ func (w *WhileNode) Copy() Node {
return w.tr.newWhile(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
}

// BreakNode represents a {{break}} action.
type BreakNode struct {
NodeType
Pos
tr *Tree
}

func (t *Tree) newBreak(pos Pos) *BreakNode {
return &BreakNode{NodeType: NodeBreak, Pos: pos, tr: t}
}

func (b *BreakNode) String() string {
return "{{break}}"
}

func (b *BreakNode) Copy() Node {
return b.tr.newBreak(b.Pos)
}

func (b *BreakNode) tree() *Tree {
return b.tr
}

// ContinueNode represents a {{continue}} action.
type ContinueNode struct {
NodeType
Pos
tr *Tree
}

func (t *Tree) newContinue(pos Pos) *ContinueNode {
return &ContinueNode{NodeType: NodeContinue, Pos: pos, tr: t}
}

func (c *ContinueNode) String() string {
return "{{continue}}"
}

func (c *ContinueNode) Copy() Node {
return c.tr.newContinue(c.Pos)
}

func (c *ContinueNode) tree() *Tree {
return c.tr
}

// TemplateNode represents a {{template}} action.
type TemplateNode struct {
NodeType
Expand Down

0 comments on commit d1f133b

Please sign in to comment.