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

Commit

Permalink
all: add break & continue loop actions
Browse files Browse the repository at this point in the history
  • Loading branch information
jo3-l committed Apr 3, 2022
1 parent 8e5d3cc commit 74b3d6b
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 39 deletions.
8 changes: 8 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ 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}}
The innermost {{range pipeline}} or {{while pipeline}} loop is ended early,
stopping the current iteration and bypassing all remaining iterations.
{{continue}}
The current iteration of the innermost {{range pipeline}} or {{while pipeline}}
loop is stopped, and the loop starts the next iteration.
{{try}} T1 {{catch}} T0 {{end}}
If executing T1 resulted in an error being returned from a function call,
T0 is executed with the dot set to the error.
Expand Down
79 changes: 56 additions & 23 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,18 @@ func (t *Template) DefinedTemplates() string {
return s
}

type controlFlowSignal uint8

// A set of signals that mark a disruption in the normal control flow of the program.
const (
controlFlowNone controlFlowSignal = iota
controlFlowBreak
controlFlowContinue
)

// 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) controlFlowSignal {
s.at(node)
switch node := node.(type) {
case *parse.ActionNode:
Expand All @@ -269,26 +278,34 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
case *parse.TryNode:
s.walkTry(dot, node.List, node.CatchList)
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 signal := s.walk(dot, node); signal != controlFlowNone {
return signal
}
}
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:
return controlFlowBreak
case *parse.ContinueNode:
return controlFlowContinue
default:
s.errorf("unknown node: %s", node)
}

return controlFlowNone
}

func (s *state) walkTry(dot reflect.Value, list, catchList *parse.ListNode) {
Expand All @@ -311,7 +328,7 @@ func (s *state) walkTry(dot reflect.Value, list, catchList *parse.ListNode) {

// 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) controlFlowSignal {
defer s.pop(s.mark())
val, _ := indirect(s.evalPipeline(dot, pipe))
truth, ok := isTrue(val)
Expand All @@ -320,13 +337,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 controlFlowNone
}

// IsTrue reports whether the value is 'true', in the sense of not the zero of its type,
Expand Down Expand Up @@ -364,15 +383,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) controlFlowSignal {
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) {
oneIteration := func(index, elem reflect.Value) controlFlowSignal {
s.incrOPs(1)

// Set top var (lexically the second if there are two) to the element.
Expand All @@ -383,26 +403,32 @@ 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)

signal := s.walk(elem, r.List)
s.pop(mark)
return signal
}
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 signal := oneIteration(reflect.ValueOf(i), val.Index(i)); signal == controlFlowBreak {
return controlFlowNone
}
}
return
return controlFlowNone
case reflect.Map:
if val.Len() == 0 {
break
}
for _, key := range sortKeys(val.MapKeys()) {
oneIteration(key, val.MapIndex(key))
if signal := oneIteration(key, val.MapIndex(key)); signal == controlFlowBreak {
return controlFlowNone
}
}
return
return controlFlowNone
case reflect.Chan:
if val.IsNil() {
break
Expand All @@ -413,23 +439,26 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
if !ok {
break
}
oneIteration(reflect.ValueOf(i), elem)
if signal := oneIteration(reflect.ValueOf(i), elem); signal == controlFlowBreak {
return controlFlowNone
}
}
if i == 0 {
break
}
return
return controlFlowNone
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)
}
if r.ElseList != nil {
s.walk(dot, r.ElseList)
return s.walk(dot, r.ElseList)
}
return controlFlowNone
}

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

s.at(w)
Expand All @@ -450,13 +479,17 @@ func (s *state) walkWhile(dot reflect.Value, w *parse.WhileNode) {
break
}

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

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

func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
Expand Down
4 changes: 4 additions & 0 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,8 @@ var execTests = []execTest{
{"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true},
{"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true},
{"range empty else", "{{range .SIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true},
{"range []int break else", "{{range .SI}}-{{.}}-{{break}}NOTREACHED{{else}}EMPTY{{end}}", "-3-", tVal, true},
{"range []int continue else", "{{range .SI}}-{{.}}-{{continue}}NOTREACHED{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true},
{"range []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true},
{"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", tVal, true},
{"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true},
Expand All @@ -572,6 +574,8 @@ var execTests = []execTest{

// While.
{"while increment", "{{$i := 0}}{{while lt $i 5}}<{{$i}}>{{$i = add $i 1}}{{end}}", "<0><1><2><3><4>", tVal, true},
{"while increment break else", "{{$i := 0}}{{while lt $i 5}}-{{$i}}-{{break}}NOTREACHED{{else}}EMPTY{{end}}", "-0-", tVal, true},
{"while increment continue else", "{{$i := 0}}{{while lt $i 5}}-{{$i}}-{{$i = add $i 1}}{{continue}}NOTREACHED{{else}}EMPTY{{end}}", "-0--1--2--3--4-", tVal, true},
{"while with declaration", "{{$i := -5}}{{while $truth := lt $i 0}}{{$truth}}{{$i = add $i 1}}{{end}}", "truetruetruetruetrue", tVal, true},
{"while empty value", "{{while false}}NOTEXECUTED{{end}}AFTER", "AFTER", tVal, true},
{"while empty value with else", "{{while false}}NOTEXECUTED{{else}}ELSELIST{{end}}AFTER", "ELSELISTAFTER", tVal, true},
Expand Down
4 changes: 4 additions & 0 deletions parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ const (
itemKeyword // used only to delimit the keywords
itemBlock // block keyword
itemCatch // catch keyword
itemBreak // break keyword
itemContinue // continue keyword
itemDot // the cursor, spelled '.'
itemDefine // define keyword
itemElse // else keyword
Expand All @@ -78,6 +80,8 @@ const (
var key = map[string]itemType{
".": itemDot,
"block": itemBlock,
"break": itemBreak,
"continue": itemContinue,
"catch": itemCatch,
"define": itemDefine,
"else": itemElse,
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,8 +33,10 @@ var itemName = map[itemType]string{

// keywords
itemDot: ".",
itemBreak: "break",
itemBlock: "block",
itemCatch: "catch",
itemContinue: "continue",
itemDefine: "define",
itemElse: "else",
itemIf: "if",
Expand Down Expand Up @@ -205,7 +207,7 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"keywords", "{{range if else end with while try catch}}", []item{
{"keywords", "{{range if else end with break continue while try catch}}", []item{
tLeft,
mkItem(itemRange, "range"),
tSpace,
Expand All @@ -217,6 +219,10 @@ var lexTests = []lexTest{
tSpace,
mkItem(itemWith, "with"),
tSpace,
mkItem(itemBreak, "break"),
tSpace,
mkItem(itemContinue, "continue"),
tSpace,
mkItem(itemWhile, "while"),
tSpace,
mkItem(itemTry, "try"),
Expand Down
78 changes: 63 additions & 15 deletions parse/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ 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.
nodeCatch // A catch action. Not added to tree.
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 @@ -831,6 +833,35 @@ func (w *WhileNode) Copy() Node {
return w.tr.newWhile(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
}

// TryNode represents a {{try}} action and its commands.
type TryNode struct {
NodeType
Pos
tr *Tree
List *ListNode // what to attempt to execute.
CatchList *ListNode // what to execute if execution resulted in an error.
}

func (t *Tree) newTry(pos Pos, list, catchList *ListNode) *TryNode {
return &TryNode{NodeType: NodeTry, Pos: pos, tr: t, List: list, CatchList: catchList}
}

func (t *TryNode) Type() NodeType {
return NodeTry
}

func (t *TryNode) String() string {
return fmt.Sprintf("{{try}}%s{{catch}}%s{{end}}", t.List, t.CatchList)
}

func (t *TryNode) tree() *Tree {
return t.tr
}

func (t *TryNode) Copy() Node {
return t.tr.newTry(t.Pos, t.List.CopyList(), t.CatchList.CopyList())
}

// CatchNode represents a {{catch}} action. Does not appear in the final tree.
type catchNode struct {
NodeType
Expand Down Expand Up @@ -858,33 +889,50 @@ func (c *catchNode) Copy() Node {
return c.tr.newCatch(c.Pos)
}

// TryNode represents a {{try}} action and its commands.
type TryNode struct {
// BreakNode represents a {{break}} action.
type BreakNode struct {
NodeType
Pos
tr *Tree
List *ListNode // what to attempt to execute.
CatchList *ListNode // what to execute if execution resulted in an error.
tr *Tree
}

func (t *Tree) newTry(pos Pos, list, catchList *ListNode) *TryNode {
return &TryNode{NodeType: NodeTry, Pos: pos, tr: t, List: list, CatchList: catchList}
func (t *Tree) newBreak(pos Pos) *BreakNode {
return &BreakNode{NodeType: NodeBreak, Pos: pos, tr: t}
}

func (t *TryNode) Type() NodeType {
return NodeTry
func (b *BreakNode) String() string {
return "{{break}}"
}

func (t *TryNode) String() string {
return fmt.Sprintf("{{try}}%s{{catch}}%s{{end}}", t.List, t.CatchList)
func (b *BreakNode) Copy() Node {
return b.tr.newBreak(b.Pos)
}

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

func (t *TryNode) Copy() Node {
return t.tr.newTry(t.Pos, t.List.CopyList(), t.CatchList.CopyList())
// 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.
Expand Down
Loading

0 comments on commit 74b3d6b

Please sign in to comment.