]> Cypherpunks repositories - gostls13.git/commitdiff
text/template: efficient reporting of line numbers
authorRob Pike <r@golang.org>
Tue, 8 Nov 2016 23:43:20 +0000 (15:43 -0800)
committerRob Pike <r@golang.org>
Tue, 15 Nov 2016 00:42:33 +0000 (00:42 +0000)
Instead of scanning the text to count newlines, which is n², keep track as we go
and store the line number in the token.

benchmark                 old ns/op      new ns/op     delta
BenchmarkParseLarge-4     1589721293     38783310      -97.56%

Fixes #17851

Change-Id: I231225c61e667535e2ce55cd2facea6d279cc59d
Reviewed-on: https://go-review.googlesource.com/33234
Run-TryBot: Rob Pike <r@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
src/text/template/exec_test.go
src/text/template/parse/lex.go
src/text/template/parse/lex_test.go
src/text/template/parse/parse.go
src/text/template/parse/parse_test.go

index 9b4da435bc2c52f5ae9ecb8ac1c23335af912c76..7d4af3bcbac92112c32d228fe3a7872cf275da77 100644 (file)
@@ -1152,7 +1152,7 @@ func TestUnterminatedStringError(t *testing.T) {
                t.Fatal("expected error")
        }
        str := err.Error()
-       if !strings.Contains(str, "X:3: unexpected unterminated raw quoted strin") {
+       if !strings.Contains(str, "X:3: unexpected unterminated raw quoted string") {
                t.Fatalf("unexpected error: %s", str)
        }
 }
index 7811cc1d4fa7114ca21beb4cafb2e944c6dba042..6fbf36d7a4af8b1d55155437029787037f6fda13 100644 (file)
@@ -13,9 +13,10 @@ import (
 
 // item represents a token or text string returned from the scanner.
 type item struct {
-       typ itemType // The type of this item.
-       pos Pos      // The starting position, in bytes, of this item in the input string.
-       val string   // The value of this item.
+       typ  itemType // The type of this item.
+       pos  Pos      // The starting position, in bytes, of this item in the input string.
+       val  string   // The value of this item.
+       line int      // The line number at the start of this item.
 }
 
 func (i item) String() string {
@@ -116,6 +117,7 @@ type lexer struct {
        lastPos    Pos       // position of most recent item returned by nextItem
        items      chan item // channel of scanned items
        parenDepth int       // nesting depth of ( ) exprs
+       line       int       // 1+number of newlines seen
 }
 
 // next returns the next rune in the input.
@@ -127,6 +129,9 @@ func (l *lexer) next() rune {
        r, w := utf8.DecodeRuneInString(l.input[l.pos:])
        l.width = Pos(w)
        l.pos += l.width
+       if r == '\n' {
+               l.line++
+       }
        return r
 }
 
@@ -140,11 +145,20 @@ func (l *lexer) peek() rune {
 // backup steps back one rune. Can only be called once per call of next.
 func (l *lexer) backup() {
        l.pos -= l.width
+       // Correct newline count.
+       if l.width == 1 && l.input[l.pos] == '\n' {
+               l.line--
+       }
 }
 
 // emit passes an item back to the client.
 func (l *lexer) emit(t itemType) {
-       l.items <- item{t, l.start, l.input[l.start:l.pos]}
+       l.items <- item{t, l.start, l.input[l.start:l.pos], l.line}
+       // Some items contain text internally. If so, count their newlines.
+       switch t {
+       case itemText, itemRawString, itemLeftDelim, itemRightDelim:
+               l.line += strings.Count(l.input[l.start:l.pos], "\n")
+       }
        l.start = l.pos
 }
 
@@ -169,17 +183,10 @@ func (l *lexer) acceptRun(valid string) {
        l.backup()
 }
 
-// lineNumber reports which line we're on, based on the position of
-// the previous item returned by nextItem. Doing it this way
-// means we don't have to worry about peek double counting.
-func (l *lexer) lineNumber() int {
-       return 1 + strings.Count(l.input[:l.lastPos], "\n")
-}
-
 // errorf returns an error token and terminates the scan by passing
 // back a nil pointer that will be the next state, terminating l.nextItem.
 func (l *lexer) errorf(format string, args ...interface{}) stateFn {
-       l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
+       l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.line}
        return nil
 }
 
@@ -212,6 +219,7 @@ func lex(name, input, left, right string) *lexer {
                leftDelim:  left,
                rightDelim: right,
                items:      make(chan item),
+               line:       1,
        }
        go l.run()
        return l
@@ -602,10 +610,14 @@ Loop:
 
 // lexRawQuote scans a raw quoted string.
 func lexRawQuote(l *lexer) stateFn {
+       startLine := l.line
 Loop:
        for {
                switch l.next() {
                case eof:
+                       // Restore line number to location of opening quote.
+                       // We will error out so it's ok just to overwrite the field.
+                       l.line = startLine
                        return l.errorf("unterminated raw quoted string")
                case '`':
                        break Loop
index e35ebf1a854ba5ed99987f5e395e50f289575c64..d655d788b3b0e6cb17596610d6d0a60def8aff91 100644 (file)
@@ -58,39 +58,46 @@ type lexTest struct {
        items []item
 }
 
+func mkItem(typ itemType, text string) item {
+       return item{
+               typ: typ,
+               val: text,
+       }
+}
+
 var (
-       tDot        = item{itemDot, 0, "."}
-       tBlock      = item{itemBlock, 0, "block"}
-       tEOF        = item{itemEOF, 0, ""}
-       tFor        = item{itemIdentifier, 0, "for"}
-       tLeft       = item{itemLeftDelim, 0, "{{"}
-       tLpar       = item{itemLeftParen, 0, "("}
-       tPipe       = item{itemPipe, 0, "|"}
-       tQuote      = item{itemString, 0, `"abc \n\t\" "`}
-       tRange      = item{itemRange, 0, "range"}
-       tRight      = item{itemRightDelim, 0, "}}"}
-       tRpar       = item{itemRightParen, 0, ")"}
-       tSpace      = item{itemSpace, 0, " "}
+       tDot        = mkItem(itemDot, ".")
+       tBlock      = mkItem(itemBlock, "block")
+       tEOF        = mkItem(itemEOF, "")
+       tFor        = mkItem(itemIdentifier, "for")
+       tLeft       = mkItem(itemLeftDelim, "{{")
+       tLpar       = mkItem(itemLeftParen, "(")
+       tPipe       = mkItem(itemPipe, "|")
+       tQuote      = mkItem(itemString, `"abc \n\t\" "`)
+       tRange      = mkItem(itemRange, "range")
+       tRight      = mkItem(itemRightDelim, "}}")
+       tRpar       = mkItem(itemRightParen, ")")
+       tSpace      = mkItem(itemSpace, " ")
        raw         = "`" + `abc\n\t\" ` + "`"
        rawNL       = "`now is{{\n}}the time`" // Contains newline inside raw quote.
-       tRawQuote   = item{itemRawString, 0, raw}
-       tRawQuoteNL = item{itemRawString, 0, rawNL}
+       tRawQuote   = mkItem(itemRawString, raw)
+       tRawQuoteNL = mkItem(itemRawString, rawNL)
 )
 
 var lexTests = []lexTest{
        {"empty", "", []item{tEOF}},
-       {"spaces", " \t\n", []item{{itemText, 0, " \t\n"}, tEOF}},
-       {"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}},
+       {"spaces", " \t\n", []item{mkItem(itemText, " \t\n"), tEOF}},
+       {"text", `now is the time`, []item{mkItem(itemText, "now is the time"), tEOF}},
        {"text with comment", "hello-{{/* this is a comment */}}-world", []item{
-               {itemText, 0, "hello-"},
-               {itemText, 0, "-world"},
+               mkItem(itemText, "hello-"),
+               mkItem(itemText, "-world"),
                tEOF,
        }},
        {"punctuation", "{{,@% }}", []item{
                tLeft,
-               {itemChar, 0, ","},
-               {itemChar, 0, "@"},
-               {itemChar, 0, "%"},
+               mkItem(itemChar, ","),
+               mkItem(itemChar, "@"),
+               mkItem(itemChar, "%"),
                tSpace,
                tRight,
                tEOF,
@@ -99,7 +106,7 @@ var lexTests = []lexTest{
                tLeft,
                tLpar,
                tLpar,
-               {itemNumber, 0, "3"},
+               mkItem(itemNumber, "3"),
                tRpar,
                tRpar,
                tRight,
@@ -108,54 +115,54 @@ var lexTests = []lexTest{
        {"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
        {"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
        {"block", `{{block "foo" .}}`, []item{
-               tLeft, tBlock, tSpace, {itemString, 0, `"foo"`}, tSpace, tDot, tRight, tEOF,
+               tLeft, tBlock, tSpace, mkItem(itemString, `"foo"`), tSpace, tDot, tRight, tEOF,
        }},
        {"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
        {"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
        {"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
        {"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{
                tLeft,
-               {itemNumber, 0, "1"},
+               mkItem(itemNumber, "1"),
                tSpace,
-               {itemNumber, 0, "02"},
+               mkItem(itemNumber, "02"),
                tSpace,
-               {itemNumber, 0, "0x14"},
+               mkItem(itemNumber, "0x14"),
                tSpace,
-               {itemNumber, 0, "-7.2i"},
+               mkItem(itemNumber, "-7.2i"),
                tSpace,
-               {itemNumber, 0, "1e3"},
+               mkItem(itemNumber, "1e3"),
                tSpace,
-               {itemNumber, 0, "+1.2e-4"},
+               mkItem(itemNumber, "+1.2e-4"),
                tSpace,
-               {itemNumber, 0, "4.2i"},
+               mkItem(itemNumber, "4.2i"),
                tSpace,
-               {itemComplex, 0, "1+2i"},
+               mkItem(itemComplex, "1+2i"),
                tRight,
                tEOF,
        }},
        {"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
                tLeft,
-               {itemCharConstant, 0, `'a'`},
+               mkItem(itemCharConstant, `'a'`),
                tSpace,
-               {itemCharConstant, 0, `'\n'`},
+               mkItem(itemCharConstant, `'\n'`),
                tSpace,
-               {itemCharConstant, 0, `'\''`},
+               mkItem(itemCharConstant, `'\''`),
                tSpace,
-               {itemCharConstant, 0, `'\\'`},
+               mkItem(itemCharConstant, `'\\'`),
                tSpace,
-               {itemCharConstant, 0, `'\u00FF'`},
+               mkItem(itemCharConstant, `'\u00FF'`),
                tSpace,
-               {itemCharConstant, 0, `'\xFF'`},
+               mkItem(itemCharConstant, `'\xFF'`),
                tSpace,
-               {itemCharConstant, 0, `'本'`},
+               mkItem(itemCharConstant, `'本'`),
                tRight,
                tEOF,
        }},
        {"bools", "{{true false}}", []item{
                tLeft,
-               {itemBool, 0, "true"},
+               mkItem(itemBool, "true"),
                tSpace,
-               {itemBool, 0, "false"},
+               mkItem(itemBool, "false"),
                tRight,
                tEOF,
        }},
@@ -167,178 +174,178 @@ var lexTests = []lexTest{
        }},
        {"nil", "{{nil}}", []item{
                tLeft,
-               {itemNil, 0, "nil"},
+               mkItem(itemNil, "nil"),
                tRight,
                tEOF,
        }},
        {"dots", "{{.x . .2 .x.y.z}}", []item{
                tLeft,
-               {itemField, 0, ".x"},
+               mkItem(itemField, ".x"),
                tSpace,
                tDot,
                tSpace,
-               {itemNumber, 0, ".2"},
+               mkItem(itemNumber, ".2"),
                tSpace,
-               {itemField, 0, ".x"},
-               {itemField, 0, ".y"},
-               {itemField, 0, ".z"},
+               mkItem(itemField, ".x"),
+               mkItem(itemField, ".y"),
+               mkItem(itemField, ".z"),
                tRight,
                tEOF,
        }},
        {"keywords", "{{range if else end with}}", []item{
                tLeft,
-               {itemRange, 0, "range"},
+               mkItem(itemRange, "range"),
                tSpace,
-               {itemIf, 0, "if"},
+               mkItem(itemIf, "if"),
                tSpace,
-               {itemElse, 0, "else"},
+               mkItem(itemElse, "else"),
                tSpace,
-               {itemEnd, 0, "end"},
+               mkItem(itemEnd, "end"),
                tSpace,
-               {itemWith, 0, "with"},
+               mkItem(itemWith, "with"),
                tRight,
                tEOF,
        }},
        {"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
                tLeft,
-               {itemVariable, 0, "$c"},
+               mkItem(itemVariable, "$c"),
                tSpace,
-               {itemColonEquals, 0, ":="},
+               mkItem(itemColonEquals, ":="),
                tSpace,
-               {itemIdentifier, 0, "printf"},
+               mkItem(itemIdentifier, "printf"),
                tSpace,
-               {itemVariable, 0, "$"},
+               mkItem(itemVariable, "$"),
                tSpace,
-               {itemVariable, 0, "$hello"},
+               mkItem(itemVariable, "$hello"),
                tSpace,
-               {itemVariable, 0, "$23"},
+               mkItem(itemVariable, "$23"),
                tSpace,
-               {itemVariable, 0, "$"},
+               mkItem(itemVariable, "$"),
                tSpace,
-               {itemVariable, 0, "$var"},
-               {itemField, 0, ".Field"},
+               mkItem(itemVariable, "$var"),
+               mkItem(itemField, ".Field"),
                tSpace,
-               {itemField, 0, ".Method"},
+               mkItem(itemField, ".Method"),
                tRight,
                tEOF,
        }},
        {"variable invocation", "{{$x 23}}", []item{
                tLeft,
-               {itemVariable, 0, "$x"},
+               mkItem(itemVariable, "$x"),
                tSpace,
-               {itemNumber, 0, "23"},
+               mkItem(itemNumber, "23"),
                tRight,
                tEOF,
        }},
        {"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
-               {itemText, 0, "intro "},
+               mkItem(itemText, "intro "),
                tLeft,
-               {itemIdentifier, 0, "echo"},
+               mkItem(itemIdentifier, "echo"),
                tSpace,
-               {itemIdentifier, 0, "hi"},
+               mkItem(itemIdentifier, "hi"),
                tSpace,
-               {itemNumber, 0, "1.2"},
+               mkItem(itemNumber, "1.2"),
                tSpace,
                tPipe,
-               {itemIdentifier, 0, "noargs"},
+               mkItem(itemIdentifier, "noargs"),
                tPipe,
-               {itemIdentifier, 0, "args"},
+               mkItem(itemIdentifier, "args"),
                tSpace,
-               {itemNumber, 0, "1"},
+               mkItem(itemNumber, "1"),
                tSpace,
-               {itemString, 0, `"hi"`},
+               mkItem(itemString, `"hi"`),
                tRight,
-               {itemText, 0, " outro"},
+               mkItem(itemText, " outro"),
                tEOF,
        }},
        {"declaration", "{{$v := 3}}", []item{
                tLeft,
-               {itemVariable, 0, "$v"},
+               mkItem(itemVariable, "$v"),
                tSpace,
-               {itemColonEquals, 0, ":="},
+               mkItem(itemColonEquals, ":="),
                tSpace,
-               {itemNumber, 0, "3"},
+               mkItem(itemNumber, "3"),
                tRight,
                tEOF,
        }},
        {"2 declarations", "{{$v , $w := 3}}", []item{
                tLeft,
-               {itemVariable, 0, "$v"},
+               mkItem(itemVariable, "$v"),
                tSpace,
-               {itemChar, 0, ","},
+               mkItem(itemChar, ","),
                tSpace,
-               {itemVariable, 0, "$w"},
+               mkItem(itemVariable, "$w"),
                tSpace,
-               {itemColonEquals, 0, ":="},
+               mkItem(itemColonEquals, ":="),
                tSpace,
-               {itemNumber, 0, "3"},
+               mkItem(itemNumber, "3"),
                tRight,
                tEOF,
        }},
        {"field of parenthesized expression", "{{(.X).Y}}", []item{
                tLeft,
                tLpar,
-               {itemField, 0, ".X"},
+               mkItem(itemField, ".X"),
                tRpar,
-               {itemField, 0, ".Y"},
+               mkItem(itemField, ".Y"),
                tRight,
                tEOF,
        }},
        {"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{
-               {itemText, 0, "hello-"},
+               mkItem(itemText, "hello-"),
                tLeft,
-               {itemNumber, 0, "3"},
+               mkItem(itemNumber, "3"),
                tRight,
-               {itemText, 0, "-world"},
+               mkItem(itemText, "-world"),
                tEOF,
        }},
        {"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
-               {itemText, 0, "hello-"},
-               {itemText, 0, "-world"},
+               mkItem(itemText, "hello-"),
+               mkItem(itemText, "-world"),
                tEOF,
        }},
        // errors
        {"badchar", "#{{\x01}}", []item{
-               {itemText, 0, "#"},
+               mkItem(itemText, "#"),
                tLeft,
-               {itemError, 0, "unrecognized character in action: U+0001"},
+               mkItem(itemError, "unrecognized character in action: U+0001"),
        }},
        {"unclosed action", "{{\n}}", []item{
                tLeft,
-               {itemError, 0, "unclosed action"},
+               mkItem(itemError, "unclosed action"),
        }},
        {"EOF in action", "{{range", []item{
                tLeft,
                tRange,
-               {itemError, 0, "unclosed action"},
+               mkItem(itemError, "unclosed action"),
        }},
        {"unclosed quote", "{{\"\n\"}}", []item{
                tLeft,
-               {itemError, 0, "unterminated quoted string"},
+               mkItem(itemError, "unterminated quoted string"),
        }},
        {"unclosed raw quote", "{{`xx}}", []item{
                tLeft,
-               {itemError, 0, "unterminated raw quoted string"},
+               mkItem(itemError, "unterminated raw quoted string"),
        }},
        {"unclosed char constant", "{{'\n}}", []item{
                tLeft,
-               {itemError, 0, "unterminated character constant"},
+               mkItem(itemError, "unterminated character constant"),
        }},
        {"bad number", "{{3k}}", []item{
                tLeft,
-               {itemError, 0, `bad number syntax: "3k"`},
+               mkItem(itemError, `bad number syntax: "3k"`),
        }},
        {"unclosed paren", "{{(3}}", []item{
                tLeft,
                tLpar,
-               {itemNumber, 0, "3"},
-               {itemError, 0, `unclosed left paren`},
+               mkItem(itemNumber, "3"),
+               mkItem(itemError, `unclosed left paren`),
        }},
        {"extra right paren", "{{3)}}", []item{
                tLeft,
-               {itemNumber, 0, "3"},
+               mkItem(itemNumber, "3"),
                tRpar,
-               {itemError, 0, `unexpected right paren U+0029 ')'`},
+               mkItem(itemError, `unexpected right paren U+0029 ')'`),
        }},
 
        // Fixed bugs
@@ -355,17 +362,17 @@ var lexTests = []lexTest{
                tEOF,
        }},
        {"text with bad comment", "hello-{{/*/}}-world", []item{
-               {itemText, 0, "hello-"},
-               {itemError, 0, `unclosed comment`},
+               mkItem(itemText, "hello-"),
+               mkItem(itemError, `unclosed comment`),
        }},
        {"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{
-               {itemText, 0, "hello-"},
-               {itemError, 0, `comment ends before closing delimiter`},
+               mkItem(itemText, "hello-"),
+               mkItem(itemError, `comment ends before closing delimiter`),
        }},
        // This one is an error that we can't catch because it breaks templates with
        // minimized JavaScript. Should have fixed it before Go 1.1.
        {"unmatched right delimiter", "hello-{.}}-world", []item{
-               {itemText, 0, "hello-{.}}-world"},
+               mkItem(itemText, "hello-{.}}-world"),
                tEOF,
        }},
 }
@@ -414,13 +421,13 @@ func TestLex(t *testing.T) {
 var lexDelimTests = []lexTest{
        {"punctuation", "$$,@%{{}}@@", []item{
                tLeftDelim,
-               {itemChar, 0, ","},
-               {itemChar, 0, "@"},
-               {itemChar, 0, "%"},
-               {itemChar, 0, "{"},
-               {itemChar, 0, "{"},
-               {itemChar, 0, "}"},
-               {itemChar, 0, "}"},
+               mkItem(itemChar, ","),
+               mkItem(itemChar, "@"),
+               mkItem(itemChar, "%"),
+               mkItem(itemChar, "{"),
+               mkItem(itemChar, "{"),
+               mkItem(itemChar, "}"),
+               mkItem(itemChar, "}"),
                tRightDelim,
                tEOF,
        }},
@@ -431,8 +438,8 @@ var lexDelimTests = []lexTest{
 }
 
 var (
-       tLeftDelim  = item{itemLeftDelim, 0, "$$"}
-       tRightDelim = item{itemRightDelim, 0, "@@"}
+       tLeftDelim  = mkItem(itemLeftDelim, "$$")
+       tRightDelim = mkItem(itemRightDelim, "@@")
 )
 
 func TestDelims(t *testing.T) {
@@ -447,21 +454,21 @@ func TestDelims(t *testing.T) {
 var lexPosTests = []lexTest{
        {"empty", "", []item{tEOF}},
        {"punctuation", "{{,@%#}}", []item{
-               {itemLeftDelim, 0, "{{"},
-               {itemChar, 2, ","},
-               {itemChar, 3, "@"},
-               {itemChar, 4, "%"},
-               {itemChar, 5, "#"},
-               {itemRightDelim, 6, "}}"},
-               {itemEOF, 8, ""},
+               {itemLeftDelim, 0, "{{", 1},
+               {itemChar, 2, ",", 1},
+               {itemChar, 3, "@", 1},
+               {itemChar, 4, "%", 1},
+               {itemChar, 5, "#", 1},
+               {itemRightDelim, 6, "}}", 1},
+               {itemEOF, 8, "", 1},
        }},
        {"sample", "0123{{hello}}xyz", []item{
-               {itemText, 0, "0123"},
-               {itemLeftDelim, 4, "{{"},
-               {itemIdentifier, 6, "hello"},
-               {itemRightDelim, 11, "}}"},
-               {itemText, 13, "xyz"},
-               {itemEOF, 16, ""},
+               {itemText, 0, "0123", 1},
+               {itemLeftDelim, 4, "{{", 1},
+               {itemIdentifier, 6, "hello", 1},
+               {itemRightDelim, 11, "}}", 1},
+               {itemText, 13, "xyz", 1},
+               {itemEOF, 16, "", 1},
        }},
 }
 
index 893564b983ad819dd01b3c5e032aee484e976450..6060c6d74b7f69e8c650bf4823bcd4c9efe0d492 100644 (file)
@@ -157,7 +157,7 @@ func (t *Tree) ErrorContext(n Node) (location, context string) {
 // errorf formats the error and terminates processing.
 func (t *Tree) errorf(format string, args ...interface{}) {
        t.Root = nil
-       format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.lex.lineNumber(), format)
+       format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format)
        panic(fmt.Errorf(format, args...))
 }
 
@@ -376,15 +376,17 @@ func (t *Tree) action() (n Node) {
                return t.withControl()
        }
        t.backup()
+       token := t.peek()
        // Do not pop variables; they persist until "end".
-       return t.newAction(t.peek().pos, t.lex.lineNumber(), t.pipeline("command"))
+       return t.newAction(token.pos, token.line, t.pipeline("command"))
 }
 
 // Pipeline:
 //     declarations? command ('|' command)*
 func (t *Tree) pipeline(context string) (pipe *PipeNode) {
        var decl []*VariableNode
-       pos := t.peekNonSpace().pos
+       token := t.peekNonSpace()
+       pos := token.pos
        // Are there declarations?
        for {
                if v := t.peekNonSpace(); v.typ == itemVariable {
@@ -413,7 +415,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
                }
                break
        }
-       pipe = t.newPipeline(pos, t.lex.lineNumber(), decl)
+       pipe = t.newPipeline(pos, token.line, decl)
        for {
                switch token := t.nextNonSpace(); token.typ {
                case itemRightDelim, itemRightParen:
@@ -450,7 +452,6 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
 
 func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
        defer t.popVars(len(t.vars))
-       line = t.lex.lineNumber()
        pipe = t.pipeline(context)
        var next Node
        list, next = t.itemList()
@@ -479,7 +480,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
                        t.errorf("expected end; found %s", next)
                }
        }
-       return pipe.Position(), line, pipe, list, elseList
+       return pipe.Position(), pipe.Line, pipe, list, elseList
 }
 
 // If:
@@ -521,9 +522,10 @@ func (t *Tree) elseControl() Node {
        peek := t.peekNonSpace()
        if peek.typ == itemIf {
                // We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
-               return t.newElse(peek.pos, t.lex.lineNumber())
+               return t.newElse(peek.pos, peek.line)
        }
-       return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
+       token := t.expect(itemRightDelim, "else")
+       return t.newElse(token.pos, token.line)
 }
 
 // Block:
@@ -550,7 +552,7 @@ func (t *Tree) blockControl() Node {
        block.add()
        block.stopParse()
 
-       return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
+       return t.newTemplate(token.pos, token.line, name, pipe)
 }
 
 // Template:
@@ -567,7 +569,7 @@ func (t *Tree) templateControl() Node {
                // Do not pop variables; they persist until "end".
                pipe = t.pipeline(context)
        }
-       return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
+       return t.newTemplate(token.pos, token.line, name, pipe)
 }
 
 func (t *Tree) parseTemplateName(token item, context string) (name string) {
index 9d856bcb3dfd927ce6019c8a249f7e6c43f8420d..81f14aca986cdcb8774f0c2562cc388bf6a4bb16 100644 (file)
@@ -484,3 +484,37 @@ func TestBlock(t *testing.T) {
                t.Errorf("inner template = %q, want %q", g, w)
        }
 }
+
+func TestLineNum(t *testing.T) {
+       const count = 100
+       text := strings.Repeat("{{printf 1234}}\n", count)
+       tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
+       if err != nil {
+               t.Fatal(err)
+       }
+       // Check the line numbers. Each line is an action containing a template, followed by text.
+       // That's two nodes per line.
+       nodes := tree.Root.Nodes
+       for i := 0; i < len(nodes); i += 2 {
+               line := 1 + i/2
+               // Action first.
+               action := nodes[i].(*ActionNode)
+               if action.Line != line {
+                       t.Fatalf("line %d: action is line %d", line, action.Line)
+               }
+               pipe := action.Pipe
+               if pipe.Line != line {
+                       t.Fatalf("line %d: pipe is line %d", line, pipe.Line)
+               }
+       }
+}
+
+func BenchmarkParseLarge(b *testing.B) {
+       text := strings.Repeat("{{1234}}\n", 10000)
+       for i := 0; i < b.N; i++ {
+               _, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
+               if err != nil {
+                       b.Fatal(err)
+               }
+       }
+}