Commit 28c1ad9d authored by Daniel Martí's avatar Daniel Martí

text/template: add variable assignments

Variables can be declared and shadowing is supported, but modifying
existing variables via assignments was not available.

This meant that modifying a variable from a nested block was not
possible:

	{{ $v := "init" }}
	{{ if true }}
		{{ $v := "changed" }}
	{{ end }}
	v: {{ $v }} {{/* "init" */}}

Introduce the "=" assignment token, such that one can now do:

	{{ $v := "init" }}
	{{ if true }}
		{{ $v = "changed" }}
	{{ end }}
	v: {{ $v }} {{/* "changed" */}}

To avoid confusion, rename PipeNode.Decl to PipeNode.Vars, as the
variables may not always be declared after this change. Also change a
few other names to better reflect the added ambiguity of variables in
pipelines.

Modifying the text/template/parse package in a backwards incompatible
manner is acceptable, given that the package godoc clearly states that
it isn't intended for general use. It's the equivalent of an internal
package, back when internal packages didn't exist yet.

To make the changes to the parse package sit well with the cmd/api test,
update except.txt with the changes that we aren't worried about.

Fixes #10608.

Change-Id: I1f83a4297ee093fd45f9993cebb78fc9a9e81295
Reviewed-on: https://go-review.googlesource.com/84480
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: 's avatarRob Pike <r@golang.org>
parent 804d0328
...@@ -11,4 +11,3 @@ compatibility. ...@@ -11,4 +11,3 @@ compatibility.
next.txt is the only file intended to be mutated. It's a list of next.txt is the only file intended to be mutated. It's a list of
features that may be added to the next version. It only affects features that may be added to the next version. It only affects
warning output from the go api tool. warning output from the go api tool.
...@@ -362,3 +362,22 @@ pkg syscall (openbsd-386-cgo), const SYS_KILL = 37 ...@@ -362,3 +362,22 @@ pkg syscall (openbsd-386-cgo), const SYS_KILL = 37
pkg syscall (openbsd-amd64), const SYS_KILL = 37 pkg syscall (openbsd-amd64), const SYS_KILL = 37
pkg syscall (openbsd-amd64-cgo), const SYS_KILL = 37 pkg syscall (openbsd-amd64-cgo), const SYS_KILL = 37
pkg unicode, const Version = "9.0.0" pkg unicode, const Version = "9.0.0"
pkg text/template/parse, method (*AssignNode) Copy() Node
pkg text/template/parse, method (*AssignNode) String() string
pkg text/template/parse, method (*VariableNode) Copy() Node
pkg text/template/parse, method (*VariableNode) String() string
pkg text/template/parse, method (AssignNode) Position() Pos
pkg text/template/parse, method (AssignNode) Type() NodeType
pkg text/template/parse, method (VariableNode) Position() Pos
pkg text/template/parse, method (VariableNode) Type() NodeType
pkg text/template/parse, type AssignNode struct
pkg text/template/parse, type AssignNode struct, Ident []string
pkg text/template/parse, type AssignNode struct, embedded NodeType
pkg text/template/parse, type AssignNode struct, embedded Pos
pkg text/template/parse, type PipeNode struct, Decl []*VariableNode
pkg text/template/parse, type PipeNode struct, Decl bool
pkg text/template/parse, type PipeNode struct, Vars []*AssignNode
pkg text/template/parse, type VariableNode struct
pkg text/template/parse, type VariableNode struct, Ident []string
pkg text/template/parse, type VariableNode struct, embedded NodeType
pkg text/template/parse, type VariableNode struct, embedded Pos
...@@ -142,7 +142,7 @@ func (e *escaper) escape(c context, n parse.Node) context { ...@@ -142,7 +142,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
// escapeAction escapes an action template node. // escapeAction escapes an action template node.
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
if len(n.Pipe.Decl) != 0 { if len(n.Pipe.Vars) != 0 {
// A local variable assignment, not an interpolation. // A local variable assignment, not an interpolation.
return c return c
} }
......
...@@ -241,6 +241,10 @@ The initialization has syntax ...@@ -241,6 +241,10 @@ The initialization has syntax
where $variable is the name of the variable. An action that declares a where $variable is the name of the variable. An action that declares a
variable produces no output. variable produces no output.
Variables previously declared can also be assigned, using the syntax
$variable = pipeline
If a "range" action initializes a variable, the variable is set to the If a "range" action initializes a variable, the variable is set to the
successive elements of the iteration. Also, a "range" may declare two successive elements of the iteration. Also, a "range" may declare two
variables, separated by a comma: variables, separated by a comma:
......
...@@ -53,8 +53,20 @@ func (s *state) pop(mark int) { ...@@ -53,8 +53,20 @@ func (s *state) pop(mark int) {
s.vars = s.vars[0:mark] s.vars = s.vars[0:mark]
} }
// setVar overwrites the top-nth variable on the stack. Used by range iterations. // setVar overwrites the last declared variable with the given name.
func (s *state) setVar(n int, value reflect.Value) { // Used by variable assignments.
func (s *state) setVar(name string, value reflect.Value) {
for i := s.mark() - 1; i >= 0; i-- {
if s.vars[i].name == name {
s.vars[i].value = value
return
}
}
s.errorf("undefined variable: %s", name)
}
// setTopVar overwrites the top-nth variable on the stack. Used by range iterations.
func (s *state) setTopVar(n int, value reflect.Value) {
s.vars[len(s.vars)-n].value = value s.vars[len(s.vars)-n].value = value
} }
...@@ -233,7 +245,7 @@ func (s *state) walk(dot reflect.Value, node parse.Node) { ...@@ -233,7 +245,7 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
// Do not pop variables so they persist until next end. // Do not pop variables so they persist until next end.
// Also, if the action declares variables, don't print the result. // Also, if the action declares variables, don't print the result.
val := s.evalPipeline(dot, node.Pipe) val := s.evalPipeline(dot, node.Pipe)
if len(node.Pipe.Decl) == 0 { if len(node.Pipe.Vars) == 0 {
s.printValue(node, val) s.printValue(node, val)
} }
case *parse.IfNode: case *parse.IfNode:
...@@ -320,12 +332,12 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { ...@@ -320,12 +332,12 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
mark := s.mark() mark := s.mark()
oneIteration := func(index, elem reflect.Value) { oneIteration := func(index, elem reflect.Value) {
// Set top var (lexically the second if there are two) to the element. // Set top var (lexically the second if there are two) to the element.
if len(r.Pipe.Decl) > 0 { if len(r.Pipe.Vars) > 0 {
s.setVar(1, elem) s.setTopVar(1, elem)
} }
// Set next var (lexically the first if there are two) to the index. // Set next var (lexically the first if there are two) to the index.
if len(r.Pipe.Decl) > 1 { if len(r.Pipe.Vars) > 1 {
s.setVar(2, index) s.setTopVar(2, index)
} }
s.walk(elem, r.List) s.walk(elem, r.List)
s.pop(mark) s.pop(mark)
...@@ -413,8 +425,12 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref ...@@ -413,8 +425,12 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
value = reflect.ValueOf(value.Interface()) // lovely! value = reflect.ValueOf(value.Interface()) // lovely!
} }
} }
for _, variable := range pipe.Decl { for _, variable := range pipe.Vars {
s.push(variable.Ident[0], value) if pipe.Decl {
s.push(variable.Ident[0], value)
} else {
s.setVar(variable.Ident[0], value)
}
} }
return value return value
} }
...@@ -438,7 +454,7 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref ...@@ -438,7 +454,7 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
case *parse.PipeNode: case *parse.PipeNode:
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored. // Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
return s.evalPipeline(dot, n) return s.evalPipeline(dot, n)
case *parse.VariableNode: case *parse.AssignNode:
return s.evalVariableNode(dot, n, cmd.Args, final) return s.evalVariableNode(dot, n, cmd.Args, final)
} }
s.at(firstWord) s.at(firstWord)
...@@ -507,7 +523,7 @@ func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args [] ...@@ -507,7 +523,7 @@ func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []
return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final) return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final)
} }
func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalVariableNode(dot reflect.Value, variable *parse.AssignNode, args []parse.Node, final reflect.Value) reflect.Value {
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields. // $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
s.at(variable) s.at(variable)
value := s.varValue(variable.Ident[0]) value := s.varValue(variable.Ident[0])
...@@ -748,7 +764,7 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle ...@@ -748,7 +764,7 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle
s.errorf("cannot assign nil to %s", typ) s.errorf("cannot assign nil to %s", typ)
case *parse.FieldNode: case *parse.FieldNode:
return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, missingVal), typ) return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, missingVal), typ)
case *parse.VariableNode: case *parse.AssignNode:
return s.validateType(s.evalVariableNode(dot, arg, nil, missingVal), typ) return s.validateType(s.evalVariableNode(dot, arg, nil, missingVal), typ)
case *parse.PipeNode: case *parse.PipeNode:
return s.validateType(s.evalPipeline(dot, arg), typ) return s.validateType(s.evalPipeline(dot, arg), typ)
...@@ -866,7 +882,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu ...@@ -866,7 +882,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu
return s.idealConstant(n) return s.idealConstant(n)
case *parse.StringNode: case *parse.StringNode:
return reflect.ValueOf(n.Text) return reflect.ValueOf(n.Text)
case *parse.VariableNode: case *parse.AssignNode:
return s.evalVariableNode(dot, n, nil, missingVal) return s.evalVariableNode(dot, n, nil, missingVal)
case *parse.PipeNode: case *parse.PipeNode:
return s.evalPipeline(dot, n) return s.evalPipeline(dot, n)
......
...@@ -304,6 +304,13 @@ var execTests = []execTest{ ...@@ -304,6 +304,13 @@ var execTests = []execTest{
{"$.I", "{{$.I}}", "17", tVal, true}, {"$.I", "{{$.I}}", "17", tVal, true},
{"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
{"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
"3", tVal, true},
{"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
"1", tVal, true},
// Type with String method. // Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true}, {"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true},
......
...@@ -42,7 +42,8 @@ const ( ...@@ -42,7 +42,8 @@ const (
itemChar // printable ASCII character; grab bag for comma etc. itemChar // printable ASCII character; grab bag for comma etc.
itemCharConstant // character constant itemCharConstant // character constant
itemComplex // complex constant (1+2i); imaginary is just a number itemComplex // complex constant (1+2i); imaginary is just a number
itemColonEquals // colon-equals (':=') introducing a declaration itemAssign // colon-equals ('=') introducing an assignment
itemDeclare // colon-equals (':=') introducing a declaration
itemEOF itemEOF
itemField // alphanumeric identifier starting with '.' itemField // alphanumeric identifier starting with '.'
itemIdentifier // alphanumeric identifier not starting with '.' itemIdentifier // alphanumeric identifier not starting with '.'
...@@ -366,11 +367,13 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -366,11 +367,13 @@ func lexInsideAction(l *lexer) stateFn {
return l.errorf("unclosed action") return l.errorf("unclosed action")
case isSpace(r): case isSpace(r):
return lexSpace return lexSpace
case r == '=':
l.emit(itemAssign)
case r == ':': case r == ':':
if l.next() != '=' { if l.next() != '=' {
return l.errorf("expected :=") return l.errorf("expected :=")
} }
l.emit(itemColonEquals) l.emit(itemDeclare)
case r == '|': case r == '|':
l.emit(itemPipe) l.emit(itemPipe)
case r == '"': case r == '"':
......
...@@ -16,7 +16,7 @@ var itemName = map[itemType]string{ ...@@ -16,7 +16,7 @@ var itemName = map[itemType]string{
itemChar: "char", itemChar: "char",
itemCharConstant: "charconst", itemCharConstant: "charconst",
itemComplex: "complex", itemComplex: "complex",
itemColonEquals: ":=", itemDeclare: ":=",
itemEOF: "EOF", itemEOF: "EOF",
itemField: "field", itemField: "field",
itemIdentifier: "identifier", itemIdentifier: "identifier",
...@@ -210,7 +210,7 @@ var lexTests = []lexTest{ ...@@ -210,7 +210,7 @@ var lexTests = []lexTest{
tLeft, tLeft,
mkItem(itemVariable, "$c"), mkItem(itemVariable, "$c"),
tSpace, tSpace,
mkItem(itemColonEquals, ":="), mkItem(itemDeclare, ":="),
tSpace, tSpace,
mkItem(itemIdentifier, "printf"), mkItem(itemIdentifier, "printf"),
tSpace, tSpace,
...@@ -262,7 +262,7 @@ var lexTests = []lexTest{ ...@@ -262,7 +262,7 @@ var lexTests = []lexTest{
tLeft, tLeft,
mkItem(itemVariable, "$v"), mkItem(itemVariable, "$v"),
tSpace, tSpace,
mkItem(itemColonEquals, ":="), mkItem(itemDeclare, ":="),
tSpace, tSpace,
mkItem(itemNumber, "3"), mkItem(itemNumber, "3"),
tRight, tRight,
...@@ -276,7 +276,7 @@ var lexTests = []lexTest{ ...@@ -276,7 +276,7 @@ var lexTests = []lexTest{
tSpace, tSpace,
mkItem(itemVariable, "$w"), mkItem(itemVariable, "$w"),
tSpace, tSpace,
mkItem(itemColonEquals, ":="), mkItem(itemDeclare, ":="),
tSpace, tSpace,
mkItem(itemNumber, "3"), mkItem(itemNumber, "3"),
tRight, tRight,
......
...@@ -145,13 +145,14 @@ type PipeNode struct { ...@@ -145,13 +145,14 @@ type PipeNode struct {
NodeType NodeType
Pos Pos
tr *Tree tr *Tree
Line int // The line number in the input. Deprecated: Kept for compatibility. Line int // The line number in the input. Deprecated: Kept for compatibility.
Decl []*VariableNode // Variable declarations in lexical order. Decl bool // The variables are being declared, not assigned
Cmds []*CommandNode // The commands in lexical order. Vars []*AssignNode // Variables in lexical order.
Cmds []*CommandNode // The commands in lexical order.
} }
func (t *Tree) newPipeline(pos Pos, line int, decl []*VariableNode) *PipeNode { func (t *Tree) newPipeline(pos Pos, line int, vars []*AssignNode) *PipeNode {
return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Decl: decl} return &PipeNode{tr: t, NodeType: NodePipe, Pos: pos, Line: line, Vars: vars}
} }
func (p *PipeNode) append(command *CommandNode) { func (p *PipeNode) append(command *CommandNode) {
...@@ -160,8 +161,8 @@ func (p *PipeNode) append(command *CommandNode) { ...@@ -160,8 +161,8 @@ func (p *PipeNode) append(command *CommandNode) {
func (p *PipeNode) String() string { func (p *PipeNode) String() string {
s := "" s := ""
if len(p.Decl) > 0 { if len(p.Vars) > 0 {
for i, v := range p.Decl { for i, v := range p.Vars {
if i > 0 { if i > 0 {
s += ", " s += ", "
} }
...@@ -186,11 +187,11 @@ func (p *PipeNode) CopyPipe() *PipeNode { ...@@ -186,11 +187,11 @@ func (p *PipeNode) CopyPipe() *PipeNode {
if p == nil { if p == nil {
return p return p
} }
var decl []*VariableNode var vars []*AssignNode
for _, d := range p.Decl { for _, d := range p.Vars {
decl = append(decl, d.Copy().(*VariableNode)) vars = append(vars, d.Copy().(*AssignNode))
} }
n := p.tr.newPipeline(p.Pos, p.Line, decl) n := p.tr.newPipeline(p.Pos, p.Line, vars)
for _, c := range p.Cmds { for _, c := range p.Cmds {
n.append(c.Copy().(*CommandNode)) n.append(c.Copy().(*CommandNode))
} }
...@@ -317,20 +318,20 @@ func (i *IdentifierNode) Copy() Node { ...@@ -317,20 +318,20 @@ func (i *IdentifierNode) Copy() Node {
return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos) return NewIdentifier(i.Ident).SetTree(i.tr).SetPos(i.Pos)
} }
// VariableNode holds a list of variable names, possibly with chained field // AssignNode holds a list of variable names, possibly with chained field
// accesses. The dollar sign is part of the (first) name. // accesses. The dollar sign is part of the (first) name.
type VariableNode struct { type AssignNode struct {
NodeType NodeType
Pos Pos
tr *Tree tr *Tree
Ident []string // Variable name and fields in lexical order. Ident []string // Variable name and fields in lexical order.
} }
func (t *Tree) newVariable(pos Pos, ident string) *VariableNode { func (t *Tree) newVariable(pos Pos, ident string) *AssignNode {
return &VariableNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")} return &AssignNode{tr: t, NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
} }
func (v *VariableNode) String() string { func (v *AssignNode) String() string {
s := "" s := ""
for i, id := range v.Ident { for i, id := range v.Ident {
if i > 0 { if i > 0 {
...@@ -341,12 +342,12 @@ func (v *VariableNode) String() string { ...@@ -341,12 +342,12 @@ func (v *VariableNode) String() string {
return s return s
} }
func (v *VariableNode) tree() *Tree { func (v *AssignNode) tree() *Tree {
return v.tr return v.tr
} }
func (v *VariableNode) Copy() Node { func (v *AssignNode) Copy() Node {
return &VariableNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)} return &AssignNode{tr: v.tr, NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
} }
// DotNode holds the special identifier '.'. // DotNode holds the special identifier '.'.
......
...@@ -383,10 +383,11 @@ func (t *Tree) action() (n Node) { ...@@ -383,10 +383,11 @@ func (t *Tree) action() (n Node) {
// Pipeline: // Pipeline:
// declarations? command ('|' command)* // declarations? command ('|' command)*
func (t *Tree) pipeline(context string) (pipe *PipeNode) { func (t *Tree) pipeline(context string) (pipe *PipeNode) {
var decl []*VariableNode decl := false
var vars []*AssignNode
token := t.peekNonSpace() token := t.peekNonSpace()
pos := token.pos pos := token.pos
// Are there declarations? // Are there declarations or assignments?
for { for {
if v := t.peekNonSpace(); v.typ == itemVariable { if v := t.peekNonSpace(); v.typ == itemVariable {
t.next() t.next()
...@@ -395,26 +396,33 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -395,26 +396,33 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
// argument variable rather than a declaration. So remember the token // argument variable rather than a declaration. So remember the token
// adjacent to the variable so we can push it back if necessary. // adjacent to the variable so we can push it back if necessary.
tokenAfterVariable := t.peek() tokenAfterVariable := t.peek()
if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") { next := t.peekNonSpace()
switch {
case next.typ == itemAssign, next.typ == itemDeclare,
next.typ == itemChar && next.val == ",":
t.nextNonSpace() t.nextNonSpace()
variable := t.newVariable(v.pos, v.val) variable := t.newVariable(v.pos, v.val)
decl = append(decl, variable) vars = append(vars, variable)
t.vars = append(t.vars, v.val) t.vars = append(t.vars, v.val)
if next.typ == itemDeclare {
decl = true
}
if next.typ == itemChar && next.val == "," { if next.typ == itemChar && next.val == "," {
if context == "range" && len(decl) < 2 { if context == "range" && len(vars) < 2 {
continue continue
} }
t.errorf("too many declarations in %s", context) t.errorf("too many declarations in %s", context)
} }
} else if tokenAfterVariable.typ == itemSpace { case tokenAfterVariable.typ == itemSpace:
t.backup3(v, tokenAfterVariable) t.backup3(v, tokenAfterVariable)
} else { default:
t.backup2(v) t.backup2(v)
} }
} }
break break
} }
pipe = t.newPipeline(pos, token.line, decl) pipe = t.newPipeline(pos, token.line, vars)
pipe.Decl = decl
for { for {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
case itemRightDelim, itemRightParen: case itemRightDelim, itemRightParen:
......
...@@ -259,9 +259,9 @@ var parseTests = []parseTest{ ...@@ -259,9 +259,9 @@ var parseTests = []parseTest{
{"adjacent args", "{{printf 3`x`}}", hasError, ""}, {"adjacent args", "{{printf 3`x`}}", hasError, ""},
{"adjacent args with .", "{{printf `x`.}}", hasError, ""}, {"adjacent args with .", "{{printf `x`.}}", hasError, ""},
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""}, {"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
// Equals (and other chars) do not assignments make (yet). // Other kinds of assignments and operators aren't available yet.
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"}, {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
{"bug0b", "{{$x = 1}}{{$x}}", hasError, ""}, {"bug0b", "{{$x += 1}}{{$x}}", hasError, ""},
{"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""}, {"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
{"bug0d", "{{$x % 3}}{{$x}}", hasError, ""}, {"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
// Check the parse fails for := rather than comma. // Check the parse fails for := rather than comma.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment