fix panic based error reporting
diff --git a/parser.go b/parser.go
index 06e5f74..c035791 100644
--- a/parser.go
+++ b/parser.go
@@ -23,6 +23,7 @@
 	"bufio"
 	"bytes"
 	"crypto/sha1"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -48,14 +49,13 @@
 	lineno      int
 	elineno     int // lineno == elineno unless there is trailing '\'.
 	linenoFixed bool
-	unBuf       []byte
-	hasUnBuf    bool
 	done        bool
 	outStmts    *[]ast
 	ifStack     []ifState
 	inDef       []string
 	defOpt      string
 	numIfNest   int
+	err         error
 }
 
 func newParser(rd io.Reader, filename string) *parser {
@@ -67,16 +67,18 @@
 	return p
 }
 
+func (p *parser) srcpos() srcpos {
+	return srcpos{
+		filename: p.mk.filename,
+		lineno:   p.lineno,
+	}
+}
+
 func (p *parser) addStatement(stmt ast) {
 	*p.outStmts = append(*p.outStmts, stmt)
 }
 
 func (p *parser) readLine() []byte {
-	if p.hasUnBuf {
-		p.hasUnBuf = false
-		return p.unBuf
-	}
-
 	if !p.linenoFixed {
 		p.lineno = p.elineno
 	}
@@ -88,7 +90,8 @@
 	if err == io.EOF {
 		p.done = true
 	} else if err != nil {
-		panic(fmt.Errorf("readline %s:%d: %v", p.mk.filename, p.lineno, err))
+		p.err = fmt.Errorf("readline %s: %v", p.srcpos(), err)
+		p.done = true
 	}
 
 	line = bytes.TrimRight(line, "\r\n")
@@ -163,22 +166,14 @@
 	return line
 }
 
-func (p *parser) unreadLine(line []byte) {
-	if p.hasUnBuf {
-		panic("unreadLine twice!")
-	}
-	p.unBuf = line
-	p.hasUnBuf = true
-}
-
-func newAssignAST(p *parser, lhsBytes []byte, rhsBytes []byte, op string) *assignAST {
+func newAssignAST(p *parser, lhsBytes []byte, rhsBytes []byte, op string) (*assignAST, error) {
 	lhs, _, err := parseExpr(lhsBytes, nil, true)
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
 	rhs, _, err := parseExpr(rhsBytes, nil, true)
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
 	opt := ""
 	if p != nil {
@@ -189,20 +184,22 @@
 		rhs: rhs,
 		op:  op,
 		opt: opt,
-	}
+	}, nil
 }
 
-func (p *parser) parseAssign(line []byte, sep, esep int) ast {
+func (p *parser) parseAssign(line []byte, sep, esep int) (ast, error) {
 	logf("parseAssign %q op:%q", line, line[sep:esep])
-	aast := newAssignAST(p, bytes.TrimSpace(line[:sep]), trimLeftSpaceBytes(line[esep:]), string(line[sep:esep]))
-	aast.filename = p.mk.filename
-	aast.lineno = p.lineno
-	return aast
+	aast, err := newAssignAST(p, bytes.TrimSpace(line[:sep]), trimLeftSpaceBytes(line[esep:]), string(line[sep:esep]))
+	if err != nil {
+		return nil, err
+	}
+	aast.srcpos = p.srcpos()
+	return aast, nil
 }
 
-func (p *parser) parseMaybeRule(line []byte, equalIndex, semicolonIndex int) ast {
+func (p *parser) parseMaybeRule(line []byte, equalIndex, semicolonIndex int) (ast, error) {
 	if len(trimSpaceBytes(line)) == 0 {
-		return nil
+		return nil, nil
 	}
 
 	expr := line
@@ -229,7 +226,7 @@
 
 	v, _, err := parseExpr(expr, nil, true)
 	if err != nil {
-		panic(fmt.Errorf("parse %s:%d %v", p.mk.filename, p.lineno, err))
+		return nil, p.srcpos().error(err)
 	}
 
 	rast := &maybeRuleAST{
@@ -237,106 +234,110 @@
 		term:      term,
 		afterTerm: afterTerm,
 	}
-	rast.filename = p.mk.filename
-	rast.lineno = p.lineno
-	return rast
+	rast.srcpos = p.srcpos()
+	return rast, nil
 }
 
-func (p *parser) parseInclude(line string, oplen int) ast {
+func (p *parser) parseInclude(line string, oplen int) {
 	// TODO(ukai): parse expr here
 	iast := &includeAST{
 		expr: line[oplen+1:],
 		op:   line[:oplen],
 	}
-	iast.filename = p.mk.filename
-	iast.lineno = p.lineno
-	return iast
+	iast.srcpos = p.srcpos()
+	p.addStatement(iast)
 }
 
-func (p *parser) parseIfdef(line []byte, oplen int) ast {
+func (p *parser) parseIfdef(line []byte, oplen int) {
 	lhs, _, err := parseExpr(trimLeftSpaceBytes(line[oplen+1:]), nil, true)
 	if err != nil {
-		panic(fmt.Errorf("ifdef parse %s:%d %v", p.mk.filename, p.lineno, err))
+		p.err = p.srcpos().error(err)
+		return
 	}
 	iast := &ifAST{
 		op:  string(line[:oplen]),
 		lhs: lhs,
 	}
-	iast.filename = p.mk.filename
-	iast.lineno = p.lineno
+	iast.srcpos = p.srcpos()
 	p.addStatement(iast)
 	p.ifStack = append(p.ifStack, ifState{ast: iast, numNest: p.numIfNest})
 	p.outStmts = &iast.trueStmts
-	return iast
 }
 
-func (p *parser) parseTwoQuotes(s string, op string) ([]string, bool) {
+func (p *parser) parseTwoQuotes(s string, op string) ([]string, bool, error) {
 	var args []string
 	for i := 0; i < 2; i++ {
 		s = strings.TrimSpace(s)
 		if s == "" {
-			return nil, false
+			return nil, false, nil
 		}
 		quote := s[0]
 		if quote != '\'' && quote != '"' {
-			return nil, false
+			return nil, false, nil
 		}
 		end := strings.IndexByte(s[1:], quote) + 1
 		if end < 0 {
-			return nil, false
+			return nil, false, nil
 		}
 		args = append(args, s[1:end])
 		s = s[end+1:]
 	}
 	if len(s) > 0 {
-		errorExit(p.mk.filename, p.lineno, `extraneous text after %q directive`, op)
+		return nil, false, p.srcpos().errorf(`extraneous text after %q directive`, op)
 	}
-	return args, true
+	return args, true, nil
 }
 
 // parse
 //  "(lhs, rhs)"
 //  "lhs, rhs"
-func (p *parser) parseEq(s string, op string) (string, string, bool) {
+func (p *parser) parseEq(s string, op string) (string, string, bool, error) {
 	if s[0] == '(' && s[len(s)-1] == ')' {
 		s = s[1 : len(s)-1]
 		term := []byte{','}
 		in := []byte(s)
 		v, n, err := parseExpr(in, term, false)
 		if err != nil {
-			return "", "", false
+			return "", "", false, err
 		}
 		lhs := v.String()
 		n++
 		n += skipSpaces(in[n:], nil)
 		v, n, err = parseExpr(in[n:], nil, false)
 		if err != nil {
-			return "", "", false
+			return "", "", false, err
 		}
 		rhs := v.String()
-		return lhs, rhs, true
+		return lhs, rhs, true, nil
 	}
-	args, ok := p.parseTwoQuotes(s, op)
+	args, ok, err := p.parseTwoQuotes(s, op)
 	if !ok {
-		return "", "", false
+		return "", "", false, err
 	}
-	return args[0], args[1], true
+	return args[0], args[1], true, nil
 }
 
-func (p *parser) parseIfeq(line string, oplen int) ast {
+func (p *parser) parseIfeq(line string, oplen int) {
 	op := line[:oplen]
-	lhsBytes, rhsBytes, ok := p.parseEq(strings.TrimSpace(line[oplen+1:]), op)
+	lhsBytes, rhsBytes, ok, err := p.parseEq(strings.TrimSpace(line[oplen+1:]), op)
+	if err != nil {
+		p.err = err
+		return
+	}
 	if !ok {
-		errorExit(p.mk.filename, p.lineno, `*** invalid syntax in conditional.`)
+		p.err = p.srcpos().errorf(`*** invalid syntax in conditional.`)
+		return
 	}
 
 	lhs, _, err := parseExpr([]byte(lhsBytes), nil, true)
 	if err != nil {
-		panic(fmt.Errorf("parse ifeq lhs %s:%d %v", p.mk.filename, p.lineno, err))
+		p.err = p.srcpos().error(err)
+		return
 	}
 	rhs, _, err := parseExpr([]byte(rhsBytes), nil, true)
 	if err != nil {
-		panic(fmt.Errorf("parse ifeq rhs %s:%d %v", p.mk.filename, p.lineno, err))
+		p.err = p.srcpos().error(err)
+		return
 	}
 
 	iast := &ifAST{
@@ -344,25 +345,30 @@
 		lhs: lhs,
 		rhs: rhs,
 	}
-	iast.filename = p.mk.filename
-	iast.lineno = p.lineno
+	iast.srcpos = p.srcpos()
 	p.addStatement(iast)
 	p.ifStack = append(p.ifStack, ifState{ast: iast, numNest: p.numIfNest})
 	p.outStmts = &iast.trueStmts
-	return iast
+	return
 }
 
-func (p *parser) checkIfStack(curKeyword string) {
+func (p *parser) checkIfStack(curKeyword string) error {
 	if len(p.ifStack) == 0 {
-		errorExit(p.mk.filename, p.lineno, `*** extraneous %q.`, curKeyword)
+		return p.srcpos().errorf(`*** extraneous %q.`, curKeyword)
 	}
+	return nil
 }
 
 func (p *parser) parseElse(line []byte) {
-	p.checkIfStack("else")
+	err := p.checkIfStack("else")
+	if err != nil {
+		p.err = err
+		return
+	}
 	state := &p.ifStack[len(p.ifStack)-1]
 	if state.inElse {
-		errorExit(p.mk.filename, p.lineno, `*** only one "else" per conditional.`)
+		p.err = p.srcpos().errorf(`*** only one "else" per conditional.`)
+		return
 	}
 	state.inElse = true
 	p.outStmts = &state.ast.falseStmts
@@ -384,11 +390,16 @@
 		return
 	}
 	p.numIfNest = 0
-	warnNoPrefix(p.mk.filename, p.lineno, "extraneous text after `else` directive")
+	warnNoPrefix(p.srcpos(), "extraneous text after `else` directive")
+	return
 }
 
 func (p *parser) parseEndif(line string) {
-	p.checkIfStack("endif")
+	err := p.checkIfStack("endif")
+	if err != nil {
+		p.err = err
+		return
+	}
 	state := p.ifStack[len(p.ifStack)-1]
 	for t := 0; t <= state.numNest; t++ {
 		p.ifStack = p.ifStack[0 : len(p.ifStack)-1]
@@ -403,6 +414,7 @@
 			}
 		}
 	}
+	return
 }
 
 type directiveFunc func(*parser, []byte) []byte
@@ -447,12 +459,12 @@
 }
 
 func includeDirective(p *parser, line []byte) []byte {
-	p.addStatement(p.parseInclude(string(line), len("include")))
+	p.parseInclude(string(line), len("include"))
 	return nil
 }
 
 func sincludeDirective(p *parser, line []byte) []byte {
-	p.addStatement(p.parseInclude(string(line), len("-include")))
+	p.parseInclude(string(line), len("-include"))
 	return nil
 }
 
@@ -522,8 +534,7 @@
 		expr:   line,
 		export: export,
 	}
-	east.filename = p.mk.filename
-	east.lineno = p.lineno
+	east.srcpos = p.srcpos()
 	p.addStatement(east)
 	return hasEqual
 }
@@ -561,7 +572,7 @@
 	if found >= 0 && s[:found] == "endef" {
 		rest := strings.TrimSpace(s[found+1:])
 		if rest != "" && rest[0] != '#' {
-			warnNoPrefix(p.mk.filename, p.lineno, "extraneous text after \"endef\" directive")
+			warnNoPrefix(p.srcpos(), "extraneous text after \"endef\" directive")
 		}
 		return true
 	}
@@ -569,11 +580,6 @@
 }
 
 func (p *parser) parse() (mk makefile, err error) {
-	defer func() {
-		if r := recover(); r != nil {
-			err = fmt.Errorf("panic in parse %s: %v", mk.filename, r)
-		}
-	}()
 	for !p.done {
 		line := p.readLine()
 
@@ -581,9 +587,12 @@
 			lineStr := string(p.processDefineLine(line))
 			if p.isEndef(lineStr) {
 				logf("multilineAssign %q", p.inDef)
-				aast := newAssignAST(p, []byte(p.inDef[0]), []byte(strings.Join(p.inDef[1:], "\n")), "=")
-				aast.filename = p.mk.filename
-				aast.lineno = p.lineno - len(p.inDef)
+				aast, err := newAssignAST(p, []byte(p.inDef[0]), []byte(strings.Join(p.inDef[1:], "\n")), "=")
+				if err != nil {
+					return makefile{}, err
+				}
+				aast.srcpos = p.srcpos()
+				aast.srcpos.lineno -= len(p.inDef)
 				p.addStatement(aast)
 				p.inDef = nil
 				p.defOpt = ""
@@ -601,14 +610,16 @@
 		if f, ok := p.isDirective(line, makeDirectives); ok {
 			line = trimSpaceBytes(p.processMakefileLine(line))
 			line = f(p, line)
+			if p.err != nil {
+				return makefile{}, p.err
+			}
 			if len(line) == 0 {
 				continue
 			}
 		}
 		if line[0] == '\t' {
 			cast := &commandAST{cmd: string(p.processRecipeLine(line[1:]))}
-			cast.filename = p.mk.filename
-			cast.lineno = p.lineno
+			cast.srcpos = p.srcpos()
 			p.addStatement(cast)
 			continue
 		}
@@ -626,7 +637,7 @@
 				parenStack = append(parenStack, ch)
 			case ')', '}':
 				if len(parenStack) == 0 {
-					warn(p.mk.filename, p.lineno, "Unmatched parens: %s", line)
+					warn(p.srcpos(), "Unmatched parens: %s", line)
 				} else {
 					cp := closeParen(parenStack[len(parenStack)-1])
 					if cp == ch {
@@ -642,7 +653,10 @@
 			case ':':
 				if i+1 < len(line) && line[i+1] == '=' {
 					if !isRule {
-						stmt = p.parseAssign(line, i, i+2)
+						stmt, err = p.parseAssign(line, i, i+2)
+						if err != nil {
+							return makefile{}, err
+						}
 					}
 				} else {
 					isRule = true
@@ -653,14 +667,20 @@
 				}
 			case '=':
 				if !isRule {
-					stmt = p.parseAssign(line, i, i+1)
+					stmt, err = p.parseAssign(line, i, i+1)
+					if err != nil {
+						return makefile{}, err
+					}
 				}
 				if equalIndex < 0 {
 					equalIndex = i
 				}
 			case '?', '+':
 				if !isRule && i+1 < len(line) && line[i+1] == '=' {
-					stmt = p.parseAssign(line, i, i+2)
+					stmt, err = p.parseAssign(line, i, i+2)
+					if err != nil {
+						return makefile{}, err
+					}
 				}
 			}
 			if stmt != nil {
@@ -669,40 +689,42 @@
 			}
 		}
 		if stmt == nil {
-			stmt = p.parseMaybeRule(line, equalIndex, semicolonIndex)
+			stmt, err = p.parseMaybeRule(line, equalIndex, semicolonIndex)
+			if err != nil {
+				return makefile{}, err
+			}
 			if stmt != nil {
 				p.addStatement(stmt)
 			}
 		}
 	}
-	return p.mk, nil
+	return p.mk, p.err
 }
 
-func defaultMakefile() string {
+func defaultMakefile() (string, error) {
 	candidates := []string{"GNUmakefile", "makefile", "Makefile"}
 	for _, filename := range candidates {
 		if exists(filename) {
-			return filename
+			return filename, nil
 		}
 	}
-	errorNoLocationExit("no targets specified and no makefile found.")
-	panic("") // Cannot be reached.
+	return "", errors.New("no targets specified and no makefile found")
 }
 
-func parseMakefileReader(rd io.Reader, name string, lineno int) (makefile, error) {
-	parser := newParser(rd, name)
-	parser.lineno = lineno
-	parser.elineno = lineno
+func parseMakefileReader(rd io.Reader, loc srcpos) (makefile, error) {
+	parser := newParser(rd, loc.filename)
+	parser.lineno = loc.lineno
+	parser.elineno = loc.lineno
 	parser.linenoFixed = true
 	return parser.parse()
 }
 
-func parseMakefileString(s string, name string, lineno int) (makefile, error) {
-	return parseMakefileReader(strings.NewReader(s), name, lineno)
+func parseMakefileString(s string, loc srcpos) (makefile, error) {
+	return parseMakefileReader(strings.NewReader(s), loc)
 }
 
-func parseMakefileBytes(s []byte, name string, lineno int) (makefile, error) {
-	return parseMakefileReader(bytes.NewReader(s), name, lineno)
+func parseMakefileBytes(s []byte, loc srcpos) (makefile, error) {
+	return parseMakefileReader(bytes.NewReader(s), loc)
 }
 
 type mkCacheEntry struct {