cue/errors: make API more suitable for localization

- change AddNew to AddNewf
- Add accepts Error, not error
- various helper funcs changed to f version
- whole-string error messages, instead of computed

Issue #52

Change-Id: Iff30a2dfd813c56770efe466b83da94cd3a23873
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2216
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/build.go b/cue/build.go
index 69bd84b..7d775c3 100644
--- a/cue/build.go
+++ b/cue/build.go
@@ -20,6 +20,7 @@
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
 
@@ -235,7 +236,7 @@
 	return n.Pos().String()
 }
 
-func resolveFiles(idx *index, p *build.Instance) error {
+func resolveFiles(idx *index, p *build.Instance) errors.Error {
 	// Link top-level declarations. As top-level entries get unified, an entry
 	// may be linked to any top-level entry of any of the files.
 	allFields := map[string]ast.Node{}
@@ -256,7 +257,7 @@
 	return nil
 }
 
-func resolveFile(idx *index, f *ast.File, p *build.Instance, allFields map[string]ast.Node) error {
+func resolveFile(idx *index, f *ast.File, p *build.Instance, allFields map[string]ast.Node) errors.Error {
 	index := map[string][]*ast.Ident{}
 	for _, u := range f.Unresolved {
 		index[u.Name] = append(index[u.Name], u)
@@ -269,7 +270,7 @@
 			}
 		}
 	}
-	var errUnused error
+	var errUnused errors.Error
 
 	for _, spec := range f.Imports {
 		id, err := strconv.Unquote(spec.Path.Value)
diff --git a/cue/build/context.go b/cue/build/context.go
index 2065c96..4f810e8 100644
--- a/cue/build/context.go
+++ b/cue/build/context.go
@@ -65,10 +65,13 @@
 
 	err := inst.complete()
 	if err != nil {
-		inst.Err = err
-		inst.Incomplete = true
+		inst.ReportError(err)
 	}
-	return err
+	if inst.Err != nil {
+		inst.Incomplete = true
+		return inst.Err
+	}
+	return nil
 }
 
 func (c *Context) init() {
diff --git a/cue/build/import.go b/cue/build/import.go
index d63f7e4..ab814e5 100644
--- a/cue/build/import.go
+++ b/cue/build/import.go
@@ -26,7 +26,7 @@
 
 type LoadFunc func(pos token.Pos, path string) *Instance
 
-func (inst *Instance) complete() error {
+func (inst *Instance) complete() errors.Error {
 	// TODO: handle case-insensitive collisions.
 	// dir := inst.Dir
 	// names := []string{}
@@ -89,7 +89,7 @@
 					continue
 				}
 				if imp.Err != nil {
-					return imp.Err
+					return nil
 				}
 				imp.ImportPath = path
 				// imp.parent = inst
diff --git a/cue/build/instance.go b/cue/build/instance.go
index f0407c0..7824b4e 100644
--- a/cue/build/instance.go
+++ b/cue/build/instance.go
@@ -15,7 +15,6 @@
 package build
 
 import (
-	"fmt"
 	pathpkg "path"
 	"path/filepath"
 	"strings"
@@ -116,13 +115,6 @@
 	return filepath.Join(inst.Root, path)
 }
 
-func (inst *Instance) chkErr(err error) error {
-	if err != nil {
-		inst.ReportError(err)
-	}
-	return err
-}
-
 func (inst *Instance) setPkg(pkg string) bool {
 	if !inst.hasName {
 		inst.hasName = true
@@ -133,14 +125,23 @@
 }
 
 // ReportError reports an error processing this instance.
-func (inst *Instance) ReportError(err error) {
-	if inst.Err == nil {
+func (inst *Instance) ReportError(err errors.Error) {
+	var list errors.List
+	switch x := inst.Err.(type) {
+	default:
+		// Should never happen, but in the worst case at least one error is
+		// recorded.
+		return
+	case nil:
 		inst.Err = err
+		return
+	case errors.List:
+		list = x
+	case errors.Error:
+		list.Add(x)
 	}
-}
-
-func (inst *Instance) errorf(pos token.Pos, format string, args ...interface{}) error {
-	return inst.chkErr(errors.E(pos, fmt.Sprintf(format, args...)))
+	list.Add(err)
+	inst.Err = list
 }
 
 // Context defines the build context for this instance. All files defined
@@ -180,15 +181,33 @@
 func (inst *Instance) AddFile(filename string, src interface{}) error {
 	c := inst.ctxt
 	file, err := parser.ParseFile(filename, src, c.parseOptions...)
-	if err == nil {
-		err = inst.addSyntax(file)
+	if err != nil {
+		// should always be an errors.List, but just in case.
+		switch x := err.(type) {
+		case errors.List:
+			for _, e := range x {
+				inst.ReportError(e)
+			}
+		case errors.Error:
+			inst.ReportError(x)
+		default:
+			e := errors.Wrapf(err, token.NoPos, "error adding file")
+			inst.ReportError(e)
+			err = e
+		}
+		return err
 	}
-	return inst.chkErr(err)
+
+	if err := inst.addSyntax(file); err != nil {
+		inst.ReportError(err)
+		return err
+	}
+	return nil
 }
 
 // addSyntax adds the given file to list of files for this instance. The package
 // name of the file must match the package name of the instance.
-func (inst *Instance) addSyntax(file *ast.File) error {
+func (inst *Instance) addSyntax(file *ast.File) errors.Error {
 	pkg := ""
 	pos := file.Pos()
 	if file.Name != nil {
@@ -196,7 +215,7 @@
 		pos = file.Name.Pos()
 	}
 	if !inst.setPkg(pkg) && pkg != inst.PkgName {
-		return inst.errorf(pos,
+		return errors.Newf(pos,
 			"package name %q conflicts with previous package name %q",
 			pkg, inst.PkgName)
 	}
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index 4f31959..e5e1a72 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -13,27 +13,33 @@
 // limitations under the License.
 
 // Package errors defines shared types for handling CUE errors.
+//
+// The pivotal error type in CUE packages is the interface type Error.
+// The information available in such errors can be most easily retrieved using
+// the Path, Positions, and Print functions.
 package errors // import "cuelang.org/go/cue/errors"
 
 import (
+	"errors"
+	"fmt"
 	"io"
 	"sort"
 	"strings"
 
 	"cuelang.org/go/cue/token"
 	"github.com/mpvl/unique"
-	"golang.org/x/exp/errors"
-	"golang.org/x/exp/errors/fmt"
 	"golang.org/x/xerrors"
 )
 
 // New is a convenience wrapper for errors.New in the core library.
+// It does not return a CUE error.
 func New(msg string) error {
 	return errors.New(msg)
 }
 
 // A Message implements the error interface as well as Message to allow
-// internationalized messages.
+// internationalized messages. A Message is typically used as an embedding
+// in a CUE message.
 type Message struct {
 	format string
 	args   []interface{}
@@ -141,6 +147,33 @@
 	}
 }
 
+// E creates a new error. The following types correspond to the following
+// methods:
+//     arg type     maps to
+//     Message      Msg
+//     string       shorthand for a format message with no arguments
+//     token.Pos    Position
+//     []token.Pos  Positions
+//     error        Unwrap/Cause
+func E(args ...interface{}) Error {
+	e := &posError{}
+	for _, a := range args {
+		switch x := a.(type) {
+		case string:
+			e.Message = NewMessage(x, nil)
+		case Message:
+			e.Message = x
+		case token.Pos:
+			e.pos = x
+		case []token.Pos:
+			e.inputs = x
+		case error:
+			e.err = x
+		}
+	}
+	return e
+}
+
 var _ Error = &posError{}
 
 // In an List, an error is represented by an *posError.
@@ -148,122 +181,41 @@
 // the offending token, and the error condition is described
 // by Msg.
 type posError struct {
-	pos token.Pos
+	pos    token.Pos
+	inputs []token.Pos
 	Message
 
 	// The underlying error that triggered this one, if any.
 	err error
 }
 
-func (p *posError) Path() []string {
-	return Path(p.err)
-}
-
-func (p *posError) InputPositions() []token.Pos { return nil }
-
-// E creates a new error.
-func E(args ...interface{}) error {
-	e := &posError{}
-	update(e, args)
-	return e
-}
-
-func update(e *posError, args []interface{}) {
-	err := e.err
-	for _, a := range args {
-		switch x := a.(type) {
-		case string:
-			e.Message = NewMessage("%s", []interface{}{x})
-		case token.Pos:
-			e.pos = x
-		case []token.Pos:
-			// TODO: do something more clever
-			if len(x) > 0 {
-				e.pos = x[0]
-			}
-		case *posError:
-			copy := *x
-			err = &copy
-			e.err = combine(e.err, err)
-		case error:
-			e.err = combine(e.err, x)
-		}
-	}
-}
-
-func combine(a, b error) error {
-	switch x := a.(type) {
-	case nil:
-		return b
-	case List:
-		x.add(toErr(b))
-		return x
-	default:
-		return List{toErr(a), toErr(b)}
-	}
-}
-
-func toErr(err error) Error {
-	if e, ok := err.(Error); ok {
-		return e
-	}
-	return &posError{err: err}
-}
-
-func (e *posError) Position() token.Pos {
-	return e.pos
-}
+func (e *posError) Path() []string              { return Path(e.err) }
+func (e *posError) InputPositions() []token.Pos { return e.inputs }
+func (e *posError) Position() token.Pos         { return e.pos }
+func (e *posError) Unwrap() error               { return e.err }
+func (e *posError) Cause() error                { return e.err }
 
 // Error implements the error interface.
-func (e *posError) Error() string { return fmt.Sprint(e) }
-
-func (e *posError) FormatError(p errors.Printer) error {
-	next := e.err
-	if format, args := e.Msg(); format == "" {
-		next = errFormat(p, e.err)
-	} else {
-		p.Printf(format, args...)
-	}
-	if p.Detail() && e.pos.Filename() != "" || e.pos.IsValid() {
-		p.Printf("%s", e.pos.String())
-	}
-	return next
-
+func (e *posError) Error() string {
+	return e.Message.Error()
 }
 
-func (e posError) Unwrap() error {
-	return e.err
-}
-
-func errFormat(p errors.Printer, err error) (next error) {
-	switch v := err.(type) {
-	case errors.Formatter:
-		err = v.FormatError(p)
-	default:
-		p.Print(err)
-		err = nil
-	}
-
-	return err
-}
-
-// List is a list of *posError.
+// List is a list of Errors.
 // The zero value for an List is an empty List ready to use.
-//
 type List []Error
 
 func (p *List) add(err Error) {
 	*p = append(*p, err)
 }
 
-// AddNew adds an Error with given position and error message to an List.
-func (p *List) AddNew(pos token.Pos, msg string) {
-	p.add(&posError{pos: pos, Message: Message{format: msg}})
+// AddNewf adds an Error with given position and error message to an List.
+func (p *List) AddNewf(pos token.Pos, msg string, args ...interface{}) {
+	p.add(&posError{pos: pos, Message: Message{format: msg, args: args}})
 }
 
 // Add adds an Error with given position and error message to an List.
-func (p *List) Add(err error) {
-	p.add(toErr(err))
+func (p *List) Add(err Error) {
+	p.add(err)
 }
 
 // Reset resets an List to no errors.
@@ -341,7 +293,7 @@
 
 // RemoveMultiples sorts an List and removes all but the first error per line.
 func (p *List) RemoveMultiples() {
-	sort.Sort(p)
+	p.Sort()
 	var last Error
 	i := 0
 	for _, e := range *p {
diff --git a/cue/errors/errors_test.go b/cue/errors/errors_test.go
index f670cd8..76c7e68 100644
--- a/cue/errors/errors_test.go
+++ b/cue/errors/errors_test.go
@@ -49,7 +49,7 @@
 		// TODO: Add test cases.
 	}
 	for _, tt := range tests {
-		tt.p.AddNew(tt.args.pos, tt.args.msg)
+		tt.p.AddNewf(tt.args.pos, tt.args.msg)
 	}
 }
 
diff --git a/cue/instance.go b/cue/instance.go
index b797327..f793d3c 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -19,6 +19,8 @@
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/token"
 	"cuelang.org/go/internal"
 	"golang.org/x/exp/errors/fmt"
 )
@@ -61,15 +63,47 @@
 		i.Dir = p.Dir
 		i.Name = p.PkgName
 		if p.Err != nil {
-			i.setError(p.Err)
+			i.setListOrError(p.Err)
 		}
 	}
 	return i
 }
 
-func (inst *Instance) setError(err error) {
-	inst.Err = err
+func (inst *Instance) setListOrError(err error) {
 	inst.Incomplete = true
+	switch x := err.(type) {
+	case errors.List:
+		if inst.Err == nil {
+			inst.Err = x
+			return
+		}
+		for _, e := range x {
+			inst.setError(e)
+		}
+	case errors.Error:
+		inst.setError(x)
+	default:
+		inst.setError(errors.Wrapf(err, token.NoPos, "unknown error"))
+	}
+}
+
+func (inst *Instance) setError(err errors.Error) {
+	inst.Incomplete = true
+	var list errors.List
+	switch x := inst.Err.(type) {
+	default:
+		// Should never happen, but in the worst case at least one error is
+		// recorded.
+		return
+	case nil:
+		inst.Err = err
+		return
+	case errors.List:
+		list = x
+	case errors.Error:
+		list.Add(err)
+	}
+	inst.Err = list
 }
 
 func (inst *Instance) eval(ctx *context) evaluated {
@@ -218,9 +252,14 @@
 	}
 	i.scope = v.n
 
-	i.Err = resolveFiles(idx, p)
+	if err := resolveFiles(idx, p); err != nil {
+		i.setError(err)
+		return i
+	}
 	for _, f := range p.Files {
-		i.insertFile(f)
+		if err := i.insertFile(f); err != nil {
+			i.setListOrError(err)
+		}
 	}
 	i.complete = true
 
diff --git a/cue/load/config.go b/cue/load/config.go
index 78444a3..b2b7542 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -20,6 +20,7 @@
 	"runtime"
 
 	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
 )
 
 const (
@@ -121,7 +122,7 @@
 	return i
 }
 
-func (c Config) newErrInstance(m *match, path string, err error) *build.Instance {
+func (c Config) newErrInstance(m *match, path string, err errors.Error) *build.Instance {
 	i := c.Context.NewInstance(path, nil)
 	i.DisplayPath = path
 	i.ReportError(err)
diff --git a/cue/load/errors.go b/cue/load/errors.go
index 6196e2c..3a573d3 100644
--- a/cue/load/errors.go
+++ b/cue/load/errors.go
@@ -20,6 +20,7 @@
 	"strings"
 
 	build "cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
 
@@ -53,17 +54,21 @@
 
 // A packageError describes an error loading information about a package.
 type packageError struct {
-	ImportStack   []string  // shortest path from package named on command line to this one
-	Pos           token.Pos // position of error
-	Err           string    // the error itself
-	IsImportCycle bool      `json:"-"` // the error is an import cycle
-	Hard          bool      `json:"-"` // whether the error is soft or hard; soft errors are ignored in some places
+	ImportStack    []string  // shortest path from package named on command line to this one
+	Pos            token.Pos // position of error
+	errors.Message           // the error itself
+	IsImportCycle  bool      `json:"-"` // the error is an import cycle
+	Hard           bool      `json:"-"` // whether the error is soft or hard; soft errors are ignored in some places
 }
 
+func (p *packageError) Position() token.Pos         { return p.Pos }
+func (p *packageError) InputPositions() []token.Pos { return nil }
+func (p *packageError) Path() []string              { return nil }
+
 func (l *loader) errPkgf(importPos []token.Pos, format string, args ...interface{}) *packageError {
 	err := &packageError{
 		ImportStack: l.stk.Copy(),
-		Err:         fmt.Sprintf(format, args...),
+		Message:     errors.NewMessage(format, args),
 	}
 	err.fillPos(l.cfg.Dir, importPos)
 	return err
@@ -75,20 +80,21 @@
 	}
 }
 
+// TODO(localize)
 func (p *packageError) Error() string {
 	// Import cycles deserve special treatment.
 	if p.IsImportCycle {
-		return fmt.Sprintf("%s\npackage %s\n", p.Err, strings.Join(p.ImportStack, "\n\timports "))
+		return fmt.Sprintf("%s\npackage %s\n", p.Message, strings.Join(p.ImportStack, "\n\timports "))
 	}
 	if p.Pos.IsValid() {
 		// Omit import stack. The full path to the file where the error
 		// is the most important thing.
-		return p.Pos.String() + ": " + p.Err
+		return p.Pos.String() + ": " + p.Message.Error()
 	}
 	if len(p.ImportStack) == 0 {
-		return p.Err
+		return p.Message.Error()
 	}
-	return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Err
+	return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Message.Error()
 }
 
 // noCUEError is the error used by Import to describe a directory
@@ -101,14 +107,14 @@
 	Ignored bool // whether any Go files were ignored due to build tags
 }
 
-// func (e *noCUEError) Error() string {
-// 	msg := "no buildable CUE config files in " + e.Dir
-// 	if e.Ignored {
-// 		msg += " (.cue files ignored due to build tags)"
-// 	}
-// 	return msg
-// }
+func (e *noCUEError) Position() token.Pos         { return token.NoPos }
+func (e *noCUEError) InputPositions() []token.Pos { return nil }
+func (e *noCUEError) Path() []string              { return nil }
 
+// TODO(localize)
+func (e *noCUEError) Msg() (string, []interface{}) { return e.Error(), nil }
+
+// TODO(localize)
 func (e *noCUEError) Error() string {
 	// Count files beginning with _ and ., which we will pretend don't exist at all.
 	dummy := 0
@@ -142,7 +148,22 @@
 	Files    []string // corresponding files: Files[i] declares package Packages[i]
 }
 
+func (e *multiplePackageError) Position() token.Pos         { return token.NoPos }
+func (e *multiplePackageError) InputPositions() []token.Pos { return nil }
+func (e *multiplePackageError) Path() []string              { return nil }
+
+func (e *multiplePackageError) Msg() (string, []interface{}) {
+	return "found packages %s (%s) and %s (%s) in %s", []interface{}{
+		e.Packages[0],
+		e.Files[0],
+		e.Packages[1],
+		e.Files[1],
+		e.Dir,
+	}
+}
+
 func (e *multiplePackageError) Error() string {
 	// Error string limited to two entries for compatibility.
-	return fmt.Sprintf("found packages %s (%s) and %s (%s) in %s", e.Packages[0], e.Files[0], e.Packages[1], e.Files[1], e.Dir)
+	format, args := e.Msg()
+	return fmt.Sprintf(format, args...)
 }
diff --git a/cue/load/import.go b/cue/load/import.go
index 89f0595..5de8585 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -109,7 +109,7 @@
 	for dir := p.Dir; ctxt.isDir(dir); {
 		files, err := ctxt.readDir(dir)
 		if err != nil {
-			p.ReportError(err)
+			p.ReportError(errors.Wrapf(err, pos, "import failed reading dir %v", dir))
 			return p
 		}
 		rootFound := false
@@ -142,8 +142,10 @@
 	}
 
 	rewriteFiles(p, root, false)
-	if err := fp.finalize(); err != nil {
-		p.ReportError(err)
+	if errs := fp.finalize(); errs != nil {
+		for _, e := range errs {
+			p.ReportError(e)
+		}
 		return p
 	}
 
@@ -191,7 +193,7 @@
 	}
 }
 
-func updateDirs(c *Config, p *build.Instance, path, srcDir string, mode importMode) error {
+func updateDirs(c *Config, p *build.Instance, path, srcDir string, mode importMode) errors.Error {
 	ctxt := &c.fileSystem
 	// path := p.ImportPath
 	if path == "" {
@@ -277,7 +279,7 @@
 	c   *Config
 	pkg *build.Instance
 
-	err error
+	err errors.List
 }
 
 func newFileProcessor(c *Config, p *build.Instance) *fileProcessor {
@@ -289,13 +291,14 @@
 	}
 }
 
-func (fp *fileProcessor) finalize() error {
+func (fp *fileProcessor) finalize() errors.List {
 	p := fp.pkg
 	if fp.err != nil {
 		return fp.err
 	}
 	if len(p.CUEFiles) == 0 && !fp.c.DataFiles {
-		return &noCUEError{Package: p, Dir: p.Dir, Ignored: len(p.IgnoredCUEFiles) > 0}
+		fp.err.Add(&noCUEError{Package: p, Dir: p.Dir, Ignored: len(p.IgnoredCUEFiles) > 0})
+		return fp.err
 	}
 
 	for tag := range fp.allTags {
@@ -319,17 +322,15 @@
 	ext := nameExt(name)
 	p := fp.pkg
 
-	badFile := func(err error) bool {
-		if fp.err == nil {
-			fp.err = err
-		}
+	badFile := func(err errors.Error) bool {
+		fp.err.Add(err)
 		p.InvalidCUEFiles = append(p.InvalidCUEFiles, fullPath)
 		return true
 	}
 
 	match, data, filename, err := matchFile(fp.c, dir, name, true, fp.allFiles, fp.allTags)
 	if err != nil {
-		return badFile(err)
+		return badFile(errors.Wrapf(err, pos, "no match"))
 	}
 	if !match {
 		if ext == cueSuffix {
@@ -342,7 +343,18 @@
 
 	pf, err := parser.ParseFile(filename, data, parser.ImportsOnly, parser.ParseComments)
 	if err != nil {
-		return badFile(err)
+		// should always be an errors.List, but just in case.
+		switch x := err.(type) {
+		case errors.List:
+			for _, e := range x {
+				badFile(e)
+			}
+		case errors.Error:
+			badFile(x)
+		default:
+			badFile(errors.Wrapf(err, token.NoPos, "error adding file"))
+		}
+		return true
 	}
 
 	pkg := ""
diff --git a/cue/load/loader.go b/cue/load/loader.go
index c9a3c78..74d842f 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -58,7 +58,8 @@
 	a := []*build.Instance{}
 	for _, m := range l.importPaths(args) {
 		if m.Err != nil {
-			inst := c.newErrInstance(m, "", m.Err)
+			inst := c.newErrInstance(m, "",
+				errors.Wrapf(m.Err, token.NoPos, "no match"))
 			a = append(a, inst)
 			continue
 		}
@@ -110,7 +111,7 @@
 	for _, f := range files {
 		if !strings.HasSuffix(f, ".cue") {
 			return cfg.newErrInstance(nil, f,
-				errors.New("named files must be .cue files"))
+				errors.Newf(pos, "named files must be .cue files"))
 		}
 	}
 
@@ -124,7 +125,8 @@
 		}
 		fi, err := os.Stat(path)
 		if err != nil {
-			return cfg.newErrInstance(nil, path, err)
+			return cfg.newErrInstance(nil, path,
+				errors.Wrapf(err, pos, "could not find dir %s", path))
 		}
 		if fi.IsDir() {
 			return cfg.newErrInstance(nil, path,
@@ -136,11 +138,14 @@
 	// TODO: ModImportFromFiles(files)
 	_, err := filepath.Abs(cfg.Dir)
 	if err != nil {
-		return cfg.newErrInstance(nil, cfg.Dir, err)
+		return cfg.newErrInstance(nil, cfg.Dir,
+			errors.Wrapf(err, pos, "could convert '%s' to absolute path", cfg.Dir))
 	}
 	pkg.Dir = cfg.Dir
 	rewriteFiles(pkg, pkg.Dir, true)
-	err = fp.finalize() // ImportDir(&ctxt, dir, 0)
+	for _, err := range fp.finalize() { // ImportDir(&ctxt, dir, 0)
+		pkg.ReportError(err)
+	}
 	// TODO: Support module importing.
 	// if ModDirImportPath != nil {
 	// 	// Use the effective import path of the directory
diff --git a/cue/parser/interface.go b/cue/parser/interface.go
index 758e27f..eea578f 100644
--- a/cue/parser/interface.go
+++ b/cue/parser/interface.go
@@ -170,7 +170,7 @@
 		return nil, pp.errors
 	}
 	f.Filename = filename
-	resolve(f, pp.error)
+	resolve(f, pp.errf)
 
 	return
 }
@@ -216,7 +216,7 @@
 		p.errors.Sort()
 		return nil, p.errors.Err()
 	}
-	resolveExpr(e, p.error)
+	resolveExpr(e, p.errf)
 
 	return e, nil
 }
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 630e00a..d0a2c63 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -71,7 +71,7 @@
 		m = scanner.ScanComments
 	}
 	eh := func(pos token.Pos, msg string, args []interface{}) {
-		p.errors.AddNew(pos, fmt.Sprintf(msg, args...))
+		p.errors.AddNewf(pos, msg, args...)
 	}
 	p.scanner.Init(p.file, src, eh, m)
 
@@ -344,7 +344,7 @@
 	}
 }
 
-func (p *parser) error(pos token.Pos, msg string) {
+func (p *parser) errf(pos token.Pos, msg string, args ...interface{}) {
 	// ePos := p.file.Position(pos)
 	ePos := pos
 
@@ -362,24 +362,26 @@
 		}
 	}
 
-	p.errors.AddNew(ePos, msg)
+	p.errors.AddNewf(ePos, msg, args...)
 }
 
-func (p *parser) errorExpected(pos token.Pos, msg string) {
-	msg = "expected " + msg
-	if pos == p.pos {
-		// the error happened at the current position;
-		// make the error message more specific
-		if p.tok == token.COMMA && p.lit == "\n" {
-			msg += ", found newline"
-		} else {
-			msg += ", found '" + p.tok.String() + "'"
-			if p.tok.IsLiteral() {
-				msg += " " + p.lit
-			}
-		}
+func (p *parser) errorExpected(pos token.Pos, obj string) {
+	if pos != p.pos {
+		p.errf(pos, "expected %s", obj)
+		return
 	}
-	p.error(pos, msg)
+	// the error happened at the current position;
+	// make the error message more specific
+	if p.tok == token.COMMA && p.lit == "\n" {
+		p.errf(pos, "expected %s, found newline", obj)
+		return
+	}
+
+	if p.tok.IsLiteral() {
+		p.errf(pos, "expected %s, found '%s'", obj, p.tok)
+	} else {
+		p.errf(pos, "expected %s, found '%s' %s", obj, p.tok, p.lit)
+	}
 }
 
 func (p *parser) expect(tok token.Token) token.Pos {
@@ -395,7 +397,7 @@
 // for the common case of a missing comma before a newline.
 func (p *parser) expectClosing(tok token.Token, context string) token.Pos {
 	if p.tok != tok && p.tok == token.COMMA && p.lit == "\n" {
-		p.error(p.pos, "missing ',' before newline in "+context)
+		p.errf(p.pos, "missing ',' before newline in %s", context)
 		p.next()
 	}
 	return p.expect(tok)
@@ -423,12 +425,12 @@
 			return false
 		}
 	}
-	msg := "missing ','"
 	// TODO: find a way to detect crossing lines now we don't have a semi.
 	if p.lit == "\n" {
-		msg += " before newline"
+		p.errf(p.pos, "missing ',' before newline")
+	} else {
+		p.errf(p.pos, "missing ',' in %s", context)
 	}
-	p.error(p.pos, msg+" in "+context)
 	return true // "insert" comma and continue
 }
 
@@ -673,7 +675,7 @@
 		d := p.parseField(allowEmit)
 		if e, ok := d.(*ast.EmitDecl); ok {
 			if origEmit && !allowEmit {
-				p.error(p.pos, "only one emit allowed at top level")
+				p.errf(p.pos, "only one emit allowed at top level")
 			}
 			if !origEmit || !allowEmit {
 				d = &ast.BadDecl{From: e.Pos(), To: e.End()}
@@ -710,7 +712,7 @@
 
 		if !ok {
 			if !allowEmit {
-				p.error(pos, "expected label, found "+tok.String())
+				p.errf(pos, "expected label, found %s", tok)
 			}
 			if expr == nil {
 				expr = p.parseExpr()
@@ -798,7 +800,7 @@
 
 	case token.FOR, token.IF:
 		if !allowComprehension {
-			p.error(p.pos, "comprehension not alowed for this field")
+			p.errf(p.pos, "comprehension not alowed for this field")
 		}
 		clauses := p.parseComprehensionClauses()
 		return &ast.ComprehensionDecl{
@@ -972,7 +974,7 @@
 	if clauses := p.parseComprehensionClauses(); clauses != nil {
 		var expr ast.Expr
 		if len(elts) != 1 {
-			p.error(lbrack.Add(1), "list comprehension must have exactly one element")
+			p.errf(lbrack.Add(1), "list comprehension must have exactly one element")
 		}
 		if len(elts) > 0 {
 			expr = elts[0]
@@ -1028,7 +1030,7 @@
 			if p.tok == token.RBRACK || p.tok == token.FOR || p.tok == token.IF {
 				break
 			}
-			p.error(p.pos, "missing ',' before newline in list literal")
+			p.errf(p.pos, "missing ',' before newline in list literal")
 		} else if !p.atComma("list literal", token.RBRACK, token.FOR, token.IF) {
 			break
 		}
@@ -1217,7 +1219,7 @@
 
 		cc = p.openComments()
 		if p.tok != token.RPAREN {
-			p.error(p.pos, "expected ')' for string interpolation")
+			p.errf(p.pos, "expected ')' for string interpolation")
 		}
 		lit = p.scanner.ResumeInterpolation()
 		pos = p.pos
@@ -1284,7 +1286,7 @@
 	if p.tok == token.STRING {
 		path = p.lit
 		if !isValidImport(path) {
-			p.error(pos, "invalid import path: "+path)
+			p.errf(pos, "invalid import path: %s", path)
 		}
 		p.next()
 		p.expectComma() // call before accessing p.linecomment
@@ -1362,7 +1364,7 @@
 		p.expect(token.IDENT)
 		name = p.parseIdent()
 		if name.Name == "_" && p.mode&declarationErrorsMode != 0 {
-			p.error(p.pos, "invalid package name _")
+			p.errf(p.pos, "invalid package name _")
 		}
 		p.expectComma()
 	} else {
diff --git a/cue/parser/resolve.go b/cue/parser/resolve.go
index b7be33d..fd17c1e 100644
--- a/cue/parser/resolve.go
+++ b/cue/parser/resolve.go
@@ -26,11 +26,11 @@
 
 // resolve resolves all identifiers in a file. Unresolved identifiers are
 // recorded in Unresolved.
-func resolve(f *ast.File, errFn func(pos token.Pos, msg string)) {
+func resolve(f *ast.File, errFn func(pos token.Pos, msg string, args ...interface{})) {
 	walk(&scope{errFn: errFn}, f)
 }
 
-func resolveExpr(e ast.Expr, errFn func(pos token.Pos, msg string)) {
+func resolveExpr(e ast.Expr, errFn func(pos token.Pos, msg string, args ...interface{})) {
 	f := &ast.File{}
 	walk(&scope{file: f, errFn: errFn}, e)
 }
@@ -45,7 +45,7 @@
 	node  ast.Node
 	index map[string]ast.Node
 
-	errFn func(p token.Pos, msg string)
+	errFn func(p token.Pos, msg string, args ...interface{})
 }
 
 func newScope(f *ast.File, outer *scope, node ast.Node, decls []ast.Decl) *scope {
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index 122ddf0..cc5c433 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -648,7 +648,7 @@
 
 	var list errors.List
 	eh := func(pos token.Pos, msg string, args []interface{}) {
-		list.AddNew(pos, fmt.Sprintf(msg, args...))
+		list.AddNewf(pos, msg, args...)
 	}
 
 	var s Scanner