blob: 1e5f293573548b8935917d3a85230e6c09ee75c5 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package parse implements the VDL parser, converting source files into a parse
// tree. The ParseFile function is the main entry point.
package parse
//go:generate ./grammar_gen.sh
// This is the only file in this package that uses the yacc-generated parser
// with entrypoint yyParse. The result of the parse is the simple parse.File
// representation, which is used by the compilation stage.
//
// TODO(toddw): The yacc-generated parser returns pretty lousy error messages;
// basically "syntax error" is the only string returned. Improve them.
import (
"fmt"
"io"
"log"
"math/big"
"path"
"strconv"
"strings"
"text/scanner"
"v.io/x/ref/lib/vdl/vdlutil"
)
// Opts specifies vdl parsing options.
type Opts struct {
ImportsOnly bool // Only parse imports; skip everything else.
}
// ParseFile takes a file name, the contents of the vdl file src, and the
// accumulated errors, and parses the vdl into a parse.File containing the parse
// tree. Returns nil if any errors are encountered, with errs containing more
// information. Otherwise returns the parsed File.
func ParseFile(fileName string, src io.Reader, opts Opts, errs *vdlutil.Errors) *File {
start := startFile
if opts.ImportsOnly {
start = startFileImports
}
return parse(fileName, src, start, errs)
}
// ParseConfig takes a file name, the contents of the config file src, and the
// accumulated errors, and parses the config into a parse.Config containing the
// parse tree. Returns nil if any errors are encountered, with errs containing
// more information. Otherwise returns the parsed Config.
func ParseConfig(fileName string, src io.Reader, opts Opts, errs *vdlutil.Errors) *Config {
start := startConfig
if opts.ImportsOnly {
start = startConfigImports
}
// Since the syntax is so similar between config files and vdl files, we just
// parse it as a vdl file and populate Config afterwards.
file := parse(fileName, src, start, errs)
if file == nil {
return nil
}
if len(file.ErrorDefs) > 0 || len(file.TypeDefs) > 0 || len(file.Interfaces) > 0 {
errs.Errorf("%s: config files may not contain error, type or interface definitions", fileName)
return nil
}
config := &Config{
FileName: fileName,
Doc: file.Doc,
ConfigDef: file.PackageDef,
Imports: file.Imports,
Config: file.ConstDefs[0].Expr,
ConstDefs: file.ConstDefs[1:],
}
if len(config.ConstDefs) == 0 {
config.ConstDefs = nil
}
if opts.ImportsOnly {
// Clear out the const expression from the config clause.
config.Config = nil
config.ConstDefs = nil
}
return config
}
func parse(fileName string, src io.Reader, startTok int, errs *vdlutil.Errors) *File {
if errs == nil {
log.Fatal("Nil errors specified for Parse")
}
origErrs := errs.NumErrors()
lex := newLexer(fileName, src, startTok, errs)
if errCode := yyParse(lex); errCode != 0 {
errs.Errorf("%s: yyParse returned error code %v", fileName, errCode)
}
lex.attachComments()
if startTok == startFile || startTok == startConfig {
vdlutil.Vlog.Printf("PARSE RESULTS\n\n%v\n\n", lex.vdlFile)
}
if origErrs != errs.NumErrors() {
return nil
}
return lex.vdlFile
}
// ParseExprs parses data into a slice of parsed const expressions. The input
// data is specified in VDL syntax, with commas separating multiple expressions.
// There must be at least one expression specified in data. Errors are returned
// in errs.
func ParseExprs(data string, errs *vdlutil.Errors) []ConstExpr {
const name = "exprs"
lex := newLexer(name, strings.NewReader(data), startExprs, errs)
if errCode := yyParse(lex); errCode != 0 {
errs.Errorf("vdl: yyParse returned error code %d", errCode)
}
return lex.exprs
}
// ExtractExprPackagePaths returns any package paths that appear in named constants
// in expr. i.e. "a/b/c".Foo => "a/b/c".
func ExtractExprPackagePaths(expr ConstExpr) []string {
var paths []string
switch e := expr.(type) {
case *ConstNamed:
if path := packageFromName(e.Name); len(path) > 0 {
paths = append(paths, path)
}
case *ConstCompositeLit:
for _, kv := range e.KVList {
paths = append(paths, ExtractExprPackagePaths(kv.Key)...)
paths = append(paths, ExtractExprPackagePaths(kv.Value)...)
}
paths = append(paths, ExtractTypePackagePaths(e.Type)...)
case *ConstIndexed:
paths = append(paths, ExtractExprPackagePaths(e.Expr)...)
paths = append(paths, ExtractExprPackagePaths(e.IndexExpr)...)
case *ConstTypeConv:
paths = append(paths, ExtractTypePackagePaths(e.Type)...)
paths = append(paths, ExtractExprPackagePaths(e.Expr)...)
case *ConstTypeObject:
paths = append(paths, ExtractTypePackagePaths(e.Type)...)
case *ConstBinaryOp:
paths = append(paths, ExtractExprPackagePaths(e.Lexpr)...)
paths = append(paths, ExtractExprPackagePaths(e.Rexpr)...)
case *ConstUnaryOp:
paths = append(paths, ExtractExprPackagePaths(e.Expr)...)
default:
// leaf expression with no embedded expressions or types.
}
return paths
}
func ExtractTypePackagePaths(typ Type) []string {
var paths []string
switch t := typ.(type) {
case *TypeNamed:
if path := packageFromName(t.Name); len(path) > 0 {
paths = append(paths, path)
}
case *TypeArray:
paths = append(paths, ExtractTypePackagePaths(t.Elem)...)
case *TypeList:
paths = append(paths, ExtractTypePackagePaths(t.Elem)...)
case *TypeSet:
paths = append(paths, ExtractTypePackagePaths(t.Key)...)
case *TypeMap:
paths = append(paths, ExtractTypePackagePaths(t.Key)...)
paths = append(paths, ExtractTypePackagePaths(t.Elem)...)
case *TypeStruct:
for _, f := range t.Fields {
paths = append(paths, ExtractTypePackagePaths(f.Type)...)
}
case *TypeUnion:
for _, f := range t.Fields {
paths = append(paths, ExtractTypePackagePaths(f.Type)...)
}
case *TypeOptional:
paths = append(paths, ExtractTypePackagePaths(t.Base)...)
default:
// leaf type with no embedded types.
}
return paths
}
func packageFromName(name string) string {
if strings.HasPrefix(name, `"`) {
if parts := strings.SplitN(name[1:], `".`, 2); len(parts) == 2 {
return parts[0]
}
}
return ""
}
// lexer implements the yyLexer interface for the yacc-generated parser.
//
// An oddity: lexer also holds the result of the parse. Most yacc examples hold
// parse results in package-scoped (global) variables, but doing that would mean
// we wouldn't be able to run separate parses concurrently. To enable that we'd
// need each invocation of yyParse to mutate its own result, but unfortunately
// the Go yacc tool doesn't provide any way to pass extra arguments to yyParse.
//
// So we cheat and hold the parse result in the lexer, and in the yacc rules we
// call lexVDLFile(yylex) to convert from the yyLexer interface back to the
// concrete lexer type, and retrieve a pointer to the parse result.
type lexer struct {
// Fields for lexing / scanning the input source file.
name string
scanner scanner.Scanner
errs *vdlutil.Errors
startTok int // One of our dummy start tokens.
started bool // Has the dummy start token already been emitted?
sawEOF bool // Have we already seen the end-of-file?
prevTok token // Previous token, used for auto-semicolons and errors.
// Fields holding the result of file and config parsing.
comments commentMap
vdlFile *File
// Field holding the result of expr parsing.
exprs []ConstExpr
}
func newLexer(fileName string, src io.Reader, startTok int, errs *vdlutil.Errors) *lexer {
l := &lexer{name: fileName, errs: errs, startTok: startTok, vdlFile: &File{BaseName: path.Base(fileName)}}
l.comments.init()
l.scanner.Init(src)
// Don't produce character literal tokens, but do scan comments.
l.scanner.Mode = scanner.ScanIdents | scanner.ScanFloats | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanComments
// Don't treat '\n' as whitespace, so we can auto-insert semicolons.
l.scanner.Whitespace = 1<<'\t' | 1<<'\r' | 1<<' '
l.scanner.Error = func(s *scanner.Scanner, msg string) {
l.Error(msg)
}
return l
}
type token struct {
t rune
text string
pos Pos
}
func (t token) String() string {
return fmt.Sprintf("%v %U %s", t.pos, t.t, t.text)
}
// The lex* functions below all convert the yyLexer input arg into a concrete
// lexer as their first step. The type conversion is always safe since we're
// the ones who called yyParse, and thus know the concrete type is always lexer.
// lexVDLFile retrieves the File parse result from the yyLexer interface. This
// is called in the yacc rules to fill in the parse result.
func lexVDLFile(yylex yyLexer) *File {
return yylex.(*lexer).vdlFile
}
// lexPosErrorf adds an error with positional information, on a type
// implementing the yyLexer interface. This is called in the yacc rules to
// throw errors.
func lexPosErrorf(yylex yyLexer, pos Pos, format string, v ...interface{}) {
yylex.(*lexer).posErrorf(pos, format, v...)
}
// lexGenEOF tells the lexer to generate EOF tokens from now on, as if the end
// of file had been seen. This is called in the yacc rules to terminate the
// parse even if the file still has tokens.
func lexGenEOF(yylex yyLexer) {
yylex.(*lexer).sawEOF = true
}
// lexStoreExprs stores the parsed exprs in the lexer.
func lexStoreExprs(yylex yyLexer, exprs []ConstExpr) {
yylex.(*lexer).exprs = exprs
}
var keywords = map[string]int{
"const": tCONST,
"enum": tENUM,
"error": tERROR,
"import": tIMPORT,
"interface": tINTERFACE,
"map": tMAP,
"package": tPACKAGE,
"set": tSET,
"stream": tSTREAM,
"struct": tSTRUCT,
"type": tTYPE,
"typeobject": tTYPEOBJECT,
"union": tUNION,
}
type nextRune struct {
t rune
id int
}
// knownPunct is a map of our known punctuation. We support 1 and 2 rune
// combinations, where 2 rune combos must be immediately adjacent with no
// intervening whitespace. The 2-rune combos always take precedence over the
// 1-rune combos. Every entry is a valid 1-rune combo, which is returned as-is
// without a special token id; the ascii value represents itself.
var knownPunct = map[rune][]nextRune{
';': nil,
':': nil,
',': nil,
'.': nil,
'*': nil,
'(': nil,
')': nil,
'[': nil,
']': nil,
'{': nil,
'}': nil,
'+': nil,
'-': nil,
'/': nil,
'%': nil,
'^': nil,
'?': nil,
'!': {{'=', tNE}},
'=': {{'=', tEQEQ}},
'<': {{'=', tLE}, {'<', tLSH}},
'>': {{'=', tGE}, {'>', tRSH}},
'|': {{'|', tOROR}},
'&': {{'&', tANDAND}},
}
// autoSemi determines whether to automatically add a semicolon, based on the
// rule that semicolons are always added at the end of each line after certain
// tokens. The Go auto-semicolon rule is described here:
// http://golang.org/ref/spec#Semicolons
func autoSemi(prevTok token) bool {
return prevAutoSemi[prevTok.t] && prevTok.pos.IsValid()
}
var prevAutoSemi = map[rune]bool{
scanner.Ident: true,
scanner.Int: true,
scanner.Float: true,
scanner.String: true,
scanner.RawString: true,
')': true,
']': true,
'}': true,
'>': true,
}
const yaccEOF int = 0 // yacc interprets 0 as the end-of-file marker
func init() {
// yyDebug is defined in the yacc-generated grammar.go file. Setting it to 1
// only produces output on syntax errors; set it to 4 to generate full debug
// output. Sadly yacc doesn't give position information describing the error.
yyDebug = 1
}
// A note on the comment-tracking strategy. During lexing we generate
// commentBlocks, defined as a sequence of adjacent or abutting comments (either
// // or /**/) with no intervening tokens. Adjacent means that the previous
// comment ends on the line immediately before the next one starts, and abutting
// means that the previous comment ends on the same line as the next one starts.
//
// At the end of the parse we try to attach comment blocks to parse tree items.
// We use a heuristic that works for common cases, but isn't perfect - it
// mis-associates some styles of comments, and we don't ensure all comment
// blocks will be associated to an item.
type commentBlock struct {
text string
firstLine int
lastLine int
}
// update returns true and adds tok to this block if tok is adjacent or
// abutting, otherwise it returns false without mutating the block. Since we're
// handling newlines explicitly in the lexer, we never get comment tokens with
// trailing newlines. We can get embedded newlines via /**/ style comments.
func (cb *commentBlock) update(tok token) bool {
if cb.text == "" {
// First update in this block.
cb.text = tok.text
cb.firstLine = tok.pos.Line
cb.lastLine = tok.pos.Line + strings.Count(tok.text, "\n")
return true
}
if cb.lastLine >= tok.pos.Line-1 {
// The tok is adjacent or abutting.
if cb.lastLine == tok.pos.Line-1 {
// The tok is adjacent - need a newline.
cb.text += "\n"
cb.lastLine++
}
cb.text += tok.text
cb.lastLine += strings.Count(tok.text, "\n")
return true
}
return false
}
// commentMap keeps track of blocks of comments in a file. We store comment
// blocks in maps by first line, and by last line. Note that technically there
// could be more than one commentBlock ending on the same line, due to /**/
// style comments. We ignore this rare case and just keep the first one.
type commentMap struct {
byFirst map[int]commentBlock
byLast map[int]commentBlock
cur commentBlock
prevTokenPos Pos
}
func (cm *commentMap) init() {
cm.byFirst = make(map[int]commentBlock)
cm.byLast = make(map[int]commentBlock)
}
// addComment adds a comment token to the map, either appending to the current
// block or ending the current block and starting a new one.
func (cm *commentMap) addComment(tok token) {
if !cm.cur.update(tok) {
cm.endBlock()
if !cm.cur.update(tok) {
panic(fmt.Errorf("vdl: couldn't update current comment block with token %v", tok))
}
}
// Here's an example of why we need the special case endBlock logic.
//
// type Foo struct {
// // doc1
// A int // doc2
// // doc3
// B int
// }
//
// The problem is that without the special-case, we'd group doc2 and doc3
// together into the same block. That may actually be correct some times, but
// it's more common for doc3 to be semantically associated with field B. Thus
// if we've already seen any token on the same line as this comment block, we
// end the block immediately. This means that comments appearing on the same
// line as any other token are forced to be a single comment block.
if cm.prevTokenPos.Line == tok.pos.Line {
cm.endBlock()
}
}
func (cm *commentMap) handleToken(tok token) {
cm.endBlock()
cm.prevTokenPos = tok.pos
}
// endBlock adds the the current comment block to the map, and resets it in
// preparation for new comments to be added. In the rare case where we see
// comment blocks that either start or end on the same line, we just keep the
// first comment block that was inserted.
func (cm *commentMap) endBlock() {
_, inFirst := cm.byFirst[cm.cur.firstLine]
_, inLast := cm.byLast[cm.cur.lastLine]
if cm.cur.text != "" && !inFirst && !inLast {
cm.byFirst[cm.cur.firstLine] = cm.cur
cm.byLast[cm.cur.lastLine] = cm.cur
}
cm.cur.text = ""
cm.cur.firstLine = 0
cm.cur.lastLine = 0
}
// getDoc returns the documentation string associated with pos. Our rule is the
// last line of the documentation must end on the line immediately before pos.
// Once a comment block has been returned it isn't eligible to be attached to
// any other item, and is deleted from the map.
//
// The returned string is either empty, or is newline terminated.
func (cm *commentMap) getDoc(pos Pos) string {
block := cm.byLast[pos.Line-1]
if block.text == "" {
return ""
}
doc := block.text + "\n"
delete(cm.byFirst, block.firstLine)
delete(cm.byLast, block.lastLine)
return doc
}
// getDocSuffix returns the suffix documentation associated with pos. Our rule
// is the first line of the documentation must be on the same line as pos. Once
// a comment block has been returned it isn't eligible to be attached to any
// other item, and is deleted from the map.
//
// The returned string is either empty, or has a leading space.
func (cm *commentMap) getDocSuffix(pos Pos) string {
block := cm.byFirst[pos.Line]
if block.text == "" {
return ""
}
doc := " " + block.text
delete(cm.byFirst, block.firstLine)
delete(cm.byLast, block.lastLine)
return doc
}
// getFileDoc returns the file documentation. Our rule is that the first line
// of the documentation must occur on the first line of the file, and all other
// comments must have already been attached. Once a comment block has been
// returned it isn't eligible to be attached to any other item, and is deleted
// from the map.
//
// The returned string is either empty, or is newline terminated.
func (cm *commentMap) getFileDoc() string {
block := cm.byFirst[1]
if block.text == "" {
return ""
}
doc := block.text + "\n"
delete(cm.byFirst, block.firstLine)
delete(cm.byLast, block.lastLine)
return doc
}
func attachTypeComments(t Type, cm *commentMap, suffix bool) {
switch tu := t.(type) {
case *TypeEnum:
for _, label := range tu.Labels {
if suffix {
label.DocSuffix = cm.getDocSuffix(label.Pos)
} else {
label.Doc = cm.getDoc(label.Pos)
}
}
case *TypeArray:
attachTypeComments(tu.Elem, cm, suffix)
case *TypeList:
attachTypeComments(tu.Elem, cm, suffix)
case *TypeSet:
attachTypeComments(tu.Key, cm, suffix)
case *TypeMap:
attachTypeComments(tu.Key, cm, suffix)
attachTypeComments(tu.Elem, cm, suffix)
case *TypeStruct:
for _, field := range tu.Fields {
if suffix {
field.DocSuffix = cm.getDocSuffix(field.Pos)
} else {
field.Doc = cm.getDoc(field.Pos)
}
attachTypeComments(field.Type, cm, suffix)
}
case *TypeUnion:
for _, field := range tu.Fields {
if suffix {
field.DocSuffix = cm.getDocSuffix(field.Pos)
} else {
field.Doc = cm.getDoc(field.Pos)
}
attachTypeComments(field.Type, cm, suffix)
}
case *TypeOptional:
attachTypeComments(tu.Base, cm, suffix)
case *TypeNamed:
// Terminate the recursion at named types.
default:
panic(fmt.Errorf("vdl: unhandled type %#v", t))
}
}
// attachComments causes all comments collected during the parse to be attached
// to the appropriate parse tree items. This should only be called after the
// parse has completed.
func (l *lexer) attachComments() {
f := l.vdlFile
// First attach all suffix docs - these occur on the same line.
f.PackageDef.DocSuffix = l.comments.getDocSuffix(f.PackageDef.Pos)
for _, x := range f.Imports {
x.DocSuffix = l.comments.getDocSuffix(x.Pos)
}
for _, x := range f.ErrorDefs {
x.DocSuffix = l.comments.getDocSuffix(x.Pos)
}
for _, x := range f.TypeDefs {
x.DocSuffix = l.comments.getDocSuffix(x.Pos)
attachTypeComments(x.Type, &l.comments, true)
}
for _, x := range f.ConstDefs {
x.DocSuffix = l.comments.getDocSuffix(x.Pos)
}
for _, x := range f.Interfaces {
x.DocSuffix = l.comments.getDocSuffix(x.Pos)
for _, y := range x.Embeds {
y.DocSuffix = l.comments.getDocSuffix(y.Pos)
}
for _, y := range x.Methods {
y.DocSuffix = l.comments.getDocSuffix(y.Pos)
}
}
// Now attach the docs - these occur on the line immediately before.
f.PackageDef.Doc = l.comments.getDoc(f.PackageDef.Pos)
for _, x := range f.Imports {
x.Doc = l.comments.getDoc(x.Pos)
}
for _, x := range f.ErrorDefs {
x.Doc = l.comments.getDoc(x.Pos)
}
for _, x := range f.TypeDefs {
x.Doc = l.comments.getDoc(x.Pos)
attachTypeComments(x.Type, &l.comments, false)
}
for _, x := range f.ConstDefs {
x.Doc = l.comments.getDoc(x.Pos)
}
for _, x := range f.Interfaces {
x.Doc = l.comments.getDoc(x.Pos)
for _, y := range x.Embeds {
y.Doc = l.comments.getDoc(y.Pos)
}
for _, y := range x.Methods {
y.Doc = l.comments.getDoc(y.Pos)
}
}
// Finally attach the top-level file doc - this occurs on the first line.
f.Doc = l.comments.getFileDoc()
}
// nextToken uses the text/scanner package to scan the input for the next token.
func (l *lexer) nextToken() (tok token) {
tok.t = l.scanner.Scan()
tok.text = l.scanner.TokenText()
// Both Pos and scanner.Position start line and column numbering at 1.
tok.pos = Pos{Line: l.scanner.Position.Line, Col: l.scanner.Position.Column}
return
}
// translateToken takes the token we just scanned, and translates it into a
// token usable by yacc (lval and id). The done return arg is true when a real
// yacc token was generated, or false if we need another next/translate pass.
func (l *lexer) translateToken(tok token, lval *yySymType) (id int, done bool) {
switch tok.t {
case scanner.EOF:
l.sawEOF = true
if autoSemi(l.prevTok) {
return ';', true
}
return yaccEOF, true
case '\n':
if autoSemi(l.prevTok) {
return ';', true
}
// Returning done=false ensures next/translate will be called again so that
// this newline is skipped; id=yaccEOF is a dummy value that's ignored.
return yaccEOF, false
case scanner.String, scanner.RawString:
var err error
lval.strpos.Pos = tok.pos
lval.strpos.String, err = strconv.Unquote(tok.text)
if err != nil {
l.posErrorf(tok.pos, "can't convert token [%v] to string literal", tok)
}
return tSTRLIT, true
case scanner.Int:
lval.intpos.pos = tok.pos
lval.intpos.int = new(big.Int)
if _, ok := lval.intpos.int.SetString(tok.text, 0); !ok {
l.posErrorf(tok.pos, "can't convert token [%v] to integer literal", tok)
}
return tINTLIT, true
case scanner.Float:
lval.ratpos.pos = tok.pos
lval.ratpos.rat = new(big.Rat)
if _, ok := lval.ratpos.rat.SetString(tok.text); !ok {
l.posErrorf(tok.pos, "can't convert token [%v] to float literal", tok)
}
return tRATLIT, true
case scanner.Ident:
// Either the identifier is a known keyword, or we pass it through as IDENT.
if keytok, ok := keywords[tok.text]; ok {
lval.pos = tok.pos
return keytok, true
}
lval.strpos.Pos = tok.pos
lval.strpos.String = tok.text
return tIDENT, true
case scanner.Comment:
l.comments.addComment(tok)
// Comments aren't considered tokens, just like the '\n' case.
return yaccEOF, false
default:
// Either the rune is in our known punctuation whitelist, or we've hit a
// syntax error.
if nextRunes, ok := knownPunct[tok.t]; ok {
// Peek at the next rune and compare against our list of next runes. If
// we find a match we return the id in next, otherwise just return the
// original rune. This means that 2-rune tokens always take precedence
// over 1-rune tokens. Either way the pos is set to the original rune.
lval.pos = tok.pos
peek := l.scanner.Peek()
for _, next := range nextRunes {
if peek == next.t {
l.scanner.Next()
return next.id, true
}
}
return int(tok.t), true
}
l.posErrorf(tok.pos, "unexpected token [%v]", tok)
l.sawEOF = true
return yaccEOF, true
}
}
// Lex is part of the yyLexer interface, called by the yacc-generated parser.
func (l *lexer) Lex(lval *yySymType) int {
// Emit a dummy start token indicating what type of parse we're performing.
if !l.started {
l.started = true
switch l.startTok {
case startFileImports, startFile, startConfigImports, startConfig, startExprs:
return l.startTok
default:
panic(fmt.Errorf("vdl: unhandled parse start token %d", l.startTok))
}
}
// Always return EOF after we've scanned it. This ensures we emit EOF on the
// next Lex call after scanning EOF and adding an auto-semicolon.
if l.sawEOF {
return yaccEOF
}
// Run next/translate in a loop to handle newline-triggered auto-semicolons;
// nextToken needs to generate newline tokens so that we can trigger the
// auto-semicolon logic, but if the newline doesn't generate an auto-semicolon
// we should skip the token and move on to the next one.
for {
tok := l.nextToken()
if id, done := l.translateToken(tok, lval); done {
l.prevTok = tok
l.comments.handleToken(tok)
return id
}
}
}
// Error is part of the yyLexer interface, called by the yacc-generated parser.
// Unfortunately yacc doesn't give good error information - we dump the position
// of the previous scanned token as an approximation of where the error is.
func (l *lexer) Error(s string) {
l.posErrorf(l.prevTok.pos, "%s", s)
}
// posErrorf generates an error with file and pos info.
func (l *lexer) posErrorf(pos Pos, format string, v ...interface{}) {
var posstr string
if pos.IsValid() {
posstr = pos.String()
}
l.errs.Errorf(l.name+":"+posstr+" "+format, v...)
}