cue/build: adding positions to LoadFunc and supportive API

And add it to LoadFunc.

Issue #52

Change-Id: Icc6cf385897a6954cfa29ac5c93379d26e9bc954
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2202
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/build.go b/cue/build.go
index 59845be..67c2b52 100644
--- a/cue/build.go
+++ b/cue/build.go
@@ -33,7 +33,7 @@
 	ctxt    *build.Context
 }
 
-func dummyLoad(string) *build.Instance { return nil }
+func dummyLoad(token.Pos, string) *build.Instance { return nil }
 
 func (r *Runtime) index() *index {
 	if r.idx == nil {
diff --git a/cue/build/import.go b/cue/build/import.go
index 40c2711..d63f7e4 100644
--- a/cue/build/import.go
+++ b/cue/build/import.go
@@ -24,7 +24,7 @@
 	"cuelang.org/go/cue/token"
 )
 
-type LoadFunc func(path string) *Instance
+type LoadFunc func(pos token.Pos, path string) *Instance
 
 func (inst *Instance) complete() error {
 	// TODO: handle case-insensitive collisions.
@@ -80,14 +80,15 @@
 
 			imp := c.imports[path]
 			if imp == nil {
-				imp = inst.loadFunc(path)
+				pos := token.NoPos
+				if len(imported[path]) > 0 {
+					pos = imported[path][0]
+				}
+				imp = inst.loadFunc(pos, path)
 				if imp == nil {
 					continue
 				}
 				if imp.Err != nil {
-					if len(imported[path]) > 0 {
-						imp.Err = errors.Augment(imp.Err, imported[path][0])
-					}
 					return imp.Err
 				}
 				imp.ImportPath = path
diff --git a/cue/build_test.go b/cue/build_test.go
index e02f80d..fcb7d1e 100644
--- a/cue/build_test.go
+++ b/cue/build_test.go
@@ -166,7 +166,7 @@
 	imports map[string]*bimport
 }
 
-func (b *builder) load(path string) *build.Instance {
+func (b *builder) load(pos token.Pos, path string) *build.Instance {
 	p := b.ctxt.NewInstance(path, b.load)
 	bi := b.imports[path]
 	if bi == nil {
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index ad98138..99198e0 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -19,9 +19,8 @@
 	"io"
 	"sort"
 
-	"github.com/mpvl/unique"
-
 	"cuelang.org/go/cue/token"
+	"github.com/mpvl/unique"
 	"golang.org/x/exp/errors"
 	"golang.org/x/exp/errors/fmt"
 	"golang.org/x/xerrors"
@@ -37,6 +36,30 @@
 // The position points to the beginning of the offending value.
 type Handler func(pos token.Position, msg string)
 
+// A Message implements the error interface as well as Message to allow
+// internationalized messages.
+type Message struct {
+	format string
+	args   []interface{}
+}
+
+// NewMessage creates an error message for human consumption. The arguments
+// are for later consumption, allowing the message to be localized at a later
+// time.
+func NewMessage(format string, args []interface{}) Message {
+	return Message{format: format, args: args}
+}
+
+// Msg returns a printf-style format string and its arguments for human
+// consumption.
+func (m *Message) Msg() (format string, args []interface{}) {
+	return m.format, m.args
+}
+
+func (m *Message) Error() string {
+	return fmt.Sprintf(m.format, m.args...)
+}
+
 // Error is the common error message.
 type Error interface {
 	Position() token.Position
@@ -47,13 +70,39 @@
 
 // // TODO: make Error an interface that returns a list of positions.
 
+// Newf creates an Error with the associated position and message.
+func Newf(pos token.Pos, format string, args ...interface{}) Error {
+	var p token.Position
+	if pos.IsValid() {
+		p = pos.Position()
+	}
+	return &posError{
+		pos:     p,
+		Message: NewMessage(format, args),
+	}
+}
+
+// Wrapf creates an Error with the associated position and message. The provided
+// error is added for inspection context.
+func Wrapf(err error, pos token.Pos, format string, args ...interface{}) Error {
+	var p token.Position
+	if pos.IsValid() {
+		p = pos.Position()
+	}
+	return &posError{
+		pos:     p,
+		Message: NewMessage(format, args),
+		err:     err,
+	}
+}
+
 // In an List, an error is represented by an *posError.
 // The position Pos, if valid, points to the beginning of
 // the offending token, and the error condition is described
 // by Msg.
 type posError struct {
 	pos token.Position
-	msg string
+	Message
 
 	// The underlying error that triggered this one, if any.
 	err error
@@ -81,7 +130,7 @@
 	for _, a := range args {
 		switch x := a.(type) {
 		case string:
-			e.msg = x
+			e.Message = NewMessage("%s", []interface{}{x})
 		case token.Position:
 			e.pos = x
 		case []token.Position:
@@ -127,10 +176,10 @@
 
 func (e *posError) FormatError(p errors.Printer) error {
 	next := e.err
-	if e.msg == "" {
+	if format, args := e.Msg(); format == "" {
 		next = errFormat(p, e.err)
 	} else {
-		p.Print(e.msg)
+		p.Printf(format, args...)
 	}
 	if p.Detail() && e.pos.Filename != "" || e.pos.IsValid() {
 		p.Printf("%s", e.pos.String())
@@ -166,7 +215,7 @@
 
 // AddNew adds an Error with given position and error message to an List.
 func (p *List) AddNew(pos token.Position, msg string) {
-	p.add(&posError{pos: pos, msg: msg})
+	p.add(&posError{pos: pos, Message: Message{format: msg}})
 }
 
 // Add adds an Error with given position and error message to an List.
diff --git a/cue/load/import.go b/cue/load/import.go
index 9585e33..89f0595 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -16,7 +16,6 @@
 
 import (
 	"bytes"
-	"fmt"
 	"log"
 	"os"
 	pathpkg "path"
@@ -30,6 +29,7 @@
 	"cuelang.org/go/cue/ast"
 	build "cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/encoding"
+	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/cue/token"
 )
@@ -67,7 +67,7 @@
 // If an error occurs, importPkg sets the error in the returned instance,
 // which then may contain partial information.
 //
-func (l *loader) importPkg(path, srcDir string) *build.Instance {
+func (l *loader) importPkg(pos token.Pos, path, srcDir string) *build.Instance {
 	l.stk.Push(path)
 	defer l.stk.Pop()
 
@@ -117,7 +117,7 @@
 			if f.IsDir() {
 				continue
 			}
-			if fp.add(dir, f.Name(), importComment) {
+			if fp.add(pos, dir, f.Name(), importComment) {
 				root = dir
 			}
 			if f.Name() == "cue.mod" {
@@ -160,14 +160,14 @@
 // loadFunc creates a LoadFunc that can be used to create new build.Instances.
 func (l *loader) loadFunc(parentPath string) build.LoadFunc {
 
-	return func(path string) *build.Instance {
+	return func(pos token.Pos, path string) *build.Instance {
 		cfg := l.cfg
 
 		if !isLocalImport(path) {
 			// is it a builtin?
 			if strings.IndexByte(strings.Split(path, "/")[0], '.') == -1 {
 				if l.cfg.StdRoot != "" {
-					return l.importPkg(path, l.cfg.StdRoot)
+					return l.importPkg(pos, path, l.cfg.StdRoot)
 				}
 				return nil
 			}
@@ -177,7 +177,7 @@
 					"import %q not found in the pkg directory", path))
 				return i
 			}
-			return l.importPkg(path, filepath.Join(cfg.modRoot, "pkg"))
+			return l.importPkg(pos, path, filepath.Join(cfg.modRoot, "pkg"))
 		}
 
 		if strings.Contains(path, "@") {
@@ -187,7 +187,7 @@
 			return i
 		}
 
-		return l.importPkg(path, parentPath)
+		return l.importPkg(pos, path, parentPath)
 	}
 }
 
@@ -195,16 +195,16 @@
 	ctxt := &c.fileSystem
 	// path := p.ImportPath
 	if path == "" {
-		return fmt.Errorf("import %q: invalid import path", path)
+		return errors.Newf(token.NoPos, "import %q: invalid import path", path)
 	}
 
 	if ctxt.isAbsPath(path) || strings.HasPrefix(path, "/") {
-		return fmt.Errorf("load: absolute import path %q not allowed", path)
+		return errors.Newf(token.NoPos, "absolute import path %q not allowed", path)
 	}
 
 	if isLocalImport(path) {
 		if srcDir == "" {
-			return fmt.Errorf("import %q: import relative to unknown directory", path)
+			return errors.Newf(token.NoPos, "import %q: import relative to unknown directory", path)
 		}
 		if !ctxt.isAbsPath(path) {
 			p.Dir = ctxt.joinPath(srcDir, path)
@@ -219,7 +219,7 @@
 	}
 
 	// package was not found
-	return fmt.Errorf("cannot find package %q", path)
+	return errors.Newf(token.NoPos, "cannot find package %q", path)
 }
 
 func normPrefix(root, path string, isLocal bool) string {
@@ -308,7 +308,7 @@
 	return nil
 }
 
-func (fp *fileProcessor) add(root, path string, mode importMode) (added bool) {
+func (fp *fileProcessor) add(pos token.Pos, root, path string, mode importMode) (added bool) {
 	fullPath := path
 	if !filepath.IsAbs(path) {
 		fullPath = filepath.Join(root, path)
@@ -361,7 +361,7 @@
 				return false
 			}
 			// TODO: package does not conform with requested.
-			return badFile(fmt.Errorf("%s: found package %q; want %q", filename, pkg, fp.c.Package))
+			return badFile(errors.Newf(pos, "%s: found package %q; want %q", filename, pkg, fp.c.Package))
 		}
 	} else if fp.firstFile == "" {
 		p.PkgName = pkg
@@ -386,12 +386,12 @@
 		if line != 0 {
 			com, err := strconv.Unquote(qcom)
 			if err != nil {
-				badFile(fmt.Errorf("%s:%d: cannot parse import comment", filename, line))
+				badFile(errors.Newf(pos, "%s:%d: cannot parse import comment", filename, line))
 			} else if p.ImportComment == "" {
 				p.ImportComment = com
 				fp.firstCommentFile = name
 			} else if p.ImportComment != com {
-				badFile(fmt.Errorf("found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, name, p.Dir))
+				badFile(errors.Newf(pos, "found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, name, p.Dir))
 			}
 		}
 	}
diff --git a/cue/load/import_test.go b/cue/load/import_test.go
index 341999b..26caedc 100644
--- a/cue/load/import_test.go
+++ b/cue/load/import_test.go
@@ -21,6 +21,7 @@
 	"testing"
 
 	build "cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/token"
 )
 
 const testdata = "./testdata/"
@@ -28,17 +29,17 @@
 func getInst(pkg, cwd string) (*build.Instance, error) {
 	c, _ := (&Config{}).complete()
 	l := loader{cfg: c}
-	p := l.importPkg(pkg, cwd)
+	p := l.importPkg(token.NoPos, pkg, cwd)
 	return p, p.Err
 }
 
 func TestDotSlashImport(t *testing.T) {
 	c, _ := (&Config{}).complete()
 	l := loader{cfg: c}
-	p := l.importPkg(".", testdata+"other")
-	err := p.Err
-	if err != nil {
-		t.Fatal(err)
+	p := l.importPkg(token.NoPos, ".", testdata+"other")
+	errl := p.Err
+	if errl != nil {
+		t.Fatal(errl)
 	}
 	if len(p.ImportPaths) != 1 || p.ImportPaths[0] != "./file" {
 		t.Fatalf("testdata/other: Imports=%v, want [./file]", p.ImportPaths)
diff --git a/cue/load/loader.go b/cue/load/loader.go
index 670bf30..eb4597d 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -20,8 +20,6 @@
 //    - go/build
 
 import (
-	"errors"
-	"fmt"
 	"os"
 	pathpkg "path"
 	"path/filepath"
@@ -29,6 +27,7 @@
 	"unicode"
 
 	build "cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
 
@@ -112,6 +111,7 @@
 // cueFilesPackage creates a package for building a collection of CUE files
 // (typically named on the command line).
 func (l *loader) cueFilesPackage(files []string) *build.Instance {
+	pos := token.NoPos
 	cfg := l.cfg
 	// ModInit() // TODO: support modules
 	for _, f := range files {
@@ -135,9 +135,9 @@
 		}
 		if fi.IsDir() {
 			return cfg.newErrInstance(nil, path,
-				fmt.Errorf("%s is a directory, should be a CUE file", file))
+				errors.Newf(pos, "%s is a directory, should be a CUE file", file))
 		}
-		fp.add(cfg.Dir, file, allowAnonymous)
+		fp.add(pos, cfg.Dir, file, allowAnonymous)
 	}
 
 	// TODO: ModImportFromFiles(files)
diff --git a/cue/load/search.go b/cue/load/search.go
index 57ab9a5..ebb2c72 100644
--- a/cue/load/search.go
+++ b/cue/load/search.go
@@ -23,6 +23,7 @@
 	"strings"
 
 	build "cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/token"
 )
 
 // A match represents the result of matching a single package pattern.
@@ -189,7 +190,7 @@
 		// due to invalid CUE source files. This means that directories
 		// containing parse errors will be built (and fail) instead of being
 		// silently skipped as not matching the pattern.
-		p := l.importPkg("."+path[len(root):], root)
+		p := l.importPkg(token.NoPos, "."+path[len(root):], root)
 		if err := p.Err; err != nil && (p == nil || len(p.InvalidCUEFiles) == 0) {
 			switch err.(type) {
 			case nil:
@@ -329,7 +330,7 @@
 			continue
 		}
 
-		pkg := l.importPkg(a, l.cfg.Dir)
+		pkg := l.importPkg(token.NoPos, a, l.cfg.Dir)
 		out = append(out, &match{Pattern: a, Literal: true, Pkgs: []*build.Instance{pkg}})
 	}
 	return out