cue/load: implement package name qualifiers

Implements the spec.
The package name of imported files now _must_ be
included in the import path. If it it doesn't match
the last path component of the import path, it can
be modified with a :<name> at the end.

Closes #77
https://github.com/cuelang/cue/pull/77

GitOrigin-RevId: d2ad114bf6fb14ca9fc065b074fa5adcf3b4a5bc
Change-Id: I6bec03ed931e4007bc85ece6b73a1ad89c0e0952
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3000
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/build/import.go b/cue/build/import.go
index 04866b2..3f255f3 100644
--- a/cue/build/import.go
+++ b/cue/build/import.go
@@ -103,7 +103,7 @@
 					continue
 				}
 				if imp.Err != nil {
-					return nil
+					return imp.Err
 				}
 				imp.ImportPath = path
 				// imp.parent = inst
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index fc5aed1..691b7e1 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -239,6 +239,24 @@
 // The zero value for an list is an empty list ready to use.
 type list []Error
 
+func (p list) Is(err, target error) bool {
+	for _, e := range p {
+		if xerrors.Is(e, target) {
+			return true
+		}
+	}
+	return false
+}
+
+func (p list) As(err error, target interface{}) bool {
+	for _, e := range p {
+		if xerrors.As(e, target) {
+			return true
+		}
+	}
+	return false
+}
+
 // AddNewf adds an Error with given position and error message to an List.
 func (p *list) AddNewf(pos token.Pos, msg string, args ...interface{}) {
 	err := &posError{pos: pos, Message: Message{format: msg, args: args}}
diff --git a/cue/load/config.go b/cue/load/config.go
index 3433680..8b58091 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -16,8 +16,10 @@
 
 import (
 	"os"
+	pathpkg "path"
 	"path/filepath"
 	"runtime"
+	"strings"
 
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/build"
@@ -29,7 +31,7 @@
 	cueSuffix  = ".cue"
 	defaultDir = "cue"
 	modFile    = "cue.mod"
-	pkgDir     = "pkg" // TODO: vendor?
+	pkgDir     = "pkg" // TODO: vendor, third_party, _imports?
 )
 
 // FromArgsUsage is a partial usage message that applications calling
@@ -133,21 +135,186 @@
 	Overlay map[string]Source
 
 	fileSystem
+
+	loadFunc build.LoadFunc
 }
 
-func (c Config) newInstance(path string) *build.Instance {
-	i := c.Context.NewInstance(path, nil)
-	i.DisplayPath = path
+func (c *Config) newInstance(pos token.Pos, p importPath) *build.Instance {
+	dir, name, err := c.absDirFromImportPath(pos, p)
+	i := c.Context.NewInstance(dir, c.loadFunc)
+	i.Dir = dir
+	i.PkgName = name
+	i.DisplayPath = string(p)
+	i.ImportPath = string(p)
+	i.Root = c.ModuleRoot
+	i.Module = c.Module
+	i.Err = errors.Append(i.Err, err)
+
 	return i
 }
 
-func (c Config) newErrInstance(m *match, path string, err error) *build.Instance {
-	i := c.Context.NewInstance(path, nil)
-	i.DisplayPath = path
-	i.ReportError(errors.Promote(err, "instance"))
+func (c *Config) newRelInstance(pos token.Pos, path string) *build.Instance {
+	fs := c.fileSystem
+
+	var err errors.Error
+	dir := path
+
+	p := c.Context.NewInstance(path, c.loadFunc)
+	p.PkgName = c.Package
+	p.DisplayPath = filepath.ToSlash(path)
+	// p.ImportPath = string(dir) // compute unique ID.
+	p.Root = c.ModuleRoot
+	p.Module = c.Module
+
+	if isLocalImport(path) {
+		p.Local = true
+		if c.Dir == "" {
+			err = errors.Append(err, errors.Newf(pos, "cwd unknown"))
+		}
+		dir = filepath.Join(c.Dir, filepath.FromSlash(path))
+	}
+
+	if path == "" {
+		err = errors.Append(err, errors.Newf(pos,
+			"import %q: invalid import path", path))
+	} else if path != cleanImport(path) {
+		err = errors.Append(err, c.loader.errPkgf(nil,
+			"non-canonical import path: %q should be %q", path, pathpkg.Clean(path)))
+	}
+
+	if importPath, e := c.importPathFromAbsDir(fsPath(dir), path, c.Package); e != nil {
+		// Detect later to keep error messages consistent.
+	} else {
+		p.ImportPath = string(importPath)
+	}
+
+	p.Dir = dir
+
+	if fs.isAbsPath(path) || strings.HasPrefix(path, "/") {
+		err = errors.Append(err, errors.Newf(pos,
+			"absolute import path %q not allowed", path))
+	}
+	if err != nil {
+		p.Err = errors.Append(p.Err, err)
+		p.Incomplete = true
+	}
+
+	return p
+}
+
+func (c Config) newErrInstance(pos token.Pos, path importPath, err error) *build.Instance {
+	i := c.newInstance(pos, path)
+	i.Err = errors.Promote(err, "instance")
 	return i
 }
 
+func toImportPath(dir string) importPath {
+	return importPath(filepath.ToSlash(dir))
+}
+
+type importPath string
+
+type fsPath string
+
+func (c *Config) importPathFromAbsDir(absDir fsPath, key, name string) (importPath, errors.Error) {
+	if c.ModuleRoot == "" {
+		return "", errors.Newf(token.NoPos,
+			"cannot determine import path for %q (root undefined)", key)
+	}
+
+	dir := filepath.Clean(string(absDir))
+	if !strings.HasPrefix(dir, c.ModuleRoot) {
+		return "", errors.Newf(token.NoPos,
+			"cannot determine import path for %q (dir outside of root)", key)
+	}
+
+	pkg := filepath.ToSlash(dir[len(c.ModuleRoot):])
+	switch {
+	case strings.HasPrefix(pkg, "/pkg/"):
+		pkg = pkg[len("/pkg/"):]
+		if pkg == "" {
+			return "", errors.Newf(token.NoPos,
+				"invalid package %q (root of %s)", key, pkgDir)
+		}
+
+	case c.Module == "":
+		return "", errors.Newf(token.NoPos,
+			"cannot determine import path for %q (no module)", key)
+	default:
+		pkg = c.Module + pkg
+	}
+
+	return addImportQualifier(importPath(pkg), name)
+}
+
+func addImportQualifier(pkg importPath, name string) (importPath, errors.Error) {
+	if name != "" {
+		s := string(pkg)
+		if i := strings.LastIndexByte(s, '/'); i >= 0 {
+			s = s[i+1:]
+		}
+		if i := strings.LastIndexByte(s, ':'); i >= 0 {
+			// should never happen, but just in case.
+			s = s[i+1:]
+			if s != name {
+				return "", errors.Newf(token.NoPos,
+					"non-matching package names (%s != %s)", s, name)
+			}
+		} else if s != name {
+			pkg += importPath(":" + name)
+		}
+	}
+
+	return pkg, nil
+}
+
+// absDirFromImportPath converts a giving import path to an absolute directory
+// and a package name. The root directory must be set.
+//
+// The returned directory may not exist.
+func (c *Config) absDirFromImportPath(pos token.Pos, p importPath) (absDir, name string, err errors.Error) {
+	if c.ModuleRoot == "" {
+		return "", "", errors.Newf(pos, "cannot import %q (root undefined)", p)
+	}
+
+	// Extract the package name.
+
+	name = string(p)
+	switch i := strings.LastIndexAny(name, "/:"); {
+	case i < 0:
+	case p[i] == ':':
+		name = string(p[i+1:])
+		p = p[:i]
+
+	default: // p[i] == '/'
+		name = string(p[i+1:])
+	}
+
+	// TODO: fully test that name is a valid identifier.
+	if name == "" {
+		err = errors.Newf(pos, "empty package name in import path %q", p)
+	} else if strings.IndexByte(name, '.') >= 0 {
+		err = errors.Newf(pos,
+			"cannot determine package name for %q (set explicitly with ':')", p)
+	}
+
+	// Determine the directory.
+
+	sub := filepath.FromSlash(string(p))
+	switch hasPrefix := strings.HasPrefix(string(p), c.Module); {
+	case hasPrefix && len(sub) == len(c.Module):
+		absDir = c.ModuleRoot
+
+	case hasPrefix && p[len(c.Module)] == '/':
+		absDir = filepath.Join(c.ModuleRoot, sub[len(c.Module)+1:])
+
+	default:
+		absDir = filepath.Join(c.ModuleRoot, "pkg", sub)
+	}
+
+	return absDir, name, nil
+}
+
 // Complete updates the configuration information. After calling complete,
 // the following invariants hold:
 //  - c.ModuleRoot != ""
@@ -168,6 +335,8 @@
 		if err != nil {
 			return nil, err
 		}
+	} else if c.Dir, err = filepath.Abs(c.Dir); err != nil {
+		return nil, err
 	}
 
 	// TODO: we could populate this already with absolute file paths,
@@ -191,10 +360,6 @@
 
 	c.loader = &loader{cfg: &c}
 
-	if c.Context == nil {
-		c.Context = build.NewContext(build.Loader(c.loader.loadFunc(c.Dir)))
-	}
-
 	if c.cache == "" {
 		c.cache = filepath.Join(home(), defaultDir)
 	}
@@ -225,6 +390,12 @@
 		}
 	}
 
+	c.loadFunc = c.loader.loadFunc()
+
+	if c.Context == nil {
+		c.Context = build.NewContext(build.Loader(c.loadFunc))
+	}
+
 	return &c, nil
 }
 
diff --git a/cue/load/fs.go b/cue/load/fs.go
index 004866d..082efbb 100644
--- a/cue/load/fs.go
+++ b/cue/load/fs.go
@@ -68,10 +68,6 @@
 
 	// Organize overlay
 	for filename, src := range overlay {
-		if !strings.HasSuffix(filename, ".cue") {
-			return errors.Newf(token.NoPos, "overlay file %s not a .cue file", filename)
-		}
-
 		// TODO: do we need to further clean the path or check that the
 		// specified files are within the root/ absolute files?
 		dir, base := filepath.Split(filename)
diff --git a/cue/load/import.go b/cue/load/import.go
index de14a70..b392804 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -17,7 +17,6 @@
 import (
 	"bytes"
 	"log"
-	pathpkg "path"
 	"path/filepath"
 	"sort"
 	"strconv"
@@ -67,35 +66,36 @@
 // If an error occurs, importPkg sets the error in the returned instance,
 // which then may contain partial information.
 //
-func (l *loader) importPkg(pos token.Pos, path, srcDir string) *build.Instance {
-	l.stk.Push(path)
+func (l *loader) importPkg(pos token.Pos, p *build.Instance) *build.Instance {
+	l.stk.Push(p.ImportPath)
 	defer l.stk.Pop()
 
 	cfg := l.cfg
 	ctxt := &cfg.fileSystem
 
-	parentPath := path
-	if isLocalImport(path) {
-		parentPath = filepath.Join(srcDir, filepath.FromSlash(path))
-	}
-	p := cfg.Context.NewInstance(path, l.loadFunc(parentPath))
-
-	if err := updateDirs(cfg, p, path, srcDir, 0); err != nil {
-		p.ReportError(err)
+	if p.Err != nil {
 		return p
 	}
 
-	if path != cleanImport(path) {
-		report(p, l.errPkgf(nil,
-			"non-canonical import path: %q should be %q", path, pathpkg.Clean(path)))
-		p.Incomplete = true
+	info, err := ctxt.stat(p.Dir)
+	if err != nil || !info.IsDir() {
+		// package was not found
+		p.Err = errors.Newf(token.NoPos, "cannot find package %q", p.DisplayPath)
 		return p
 	}
 
 	fp := newFileProcessor(cfg, p)
 
-	root := srcDir
+	root := cfg.Dir
 
+	// If we have an explicit package name, we can ignore other packages.
+	if p.PkgName != "" {
+		fp.ignoreOther = true
+	}
+
+	// TODO: remove: use the pre-determined module root.
+	//       Also consider an additional mechanism that we may merge in packages
+	//       from parents.
 	for dir := p.Dir; ctxt.isDir(dir); {
 		files, err := ctxt.readDir(dir)
 		if err != nil {
@@ -131,8 +131,14 @@
 		dir = parent
 	}
 
-	if strings.HasPrefix(root, srcDir) {
-		root = srcDir
+	impPath, err := addImportQualifier(importPath(p.ImportPath), p.PkgName)
+	p.ImportPath = string(impPath)
+	if err != nil {
+		p.ReportError(err)
+	}
+
+	if strings.HasPrefix(root, cfg.Dir) {
+		root = cfg.Dir
 	}
 
 	rewriteFiles(p, root, false)
@@ -158,85 +164,31 @@
 }
 
 // loadFunc creates a LoadFunc that can be used to create new build.Instances.
-func (l *loader) loadFunc(parentPath string) build.LoadFunc {
+func (l *loader) loadFunc() build.LoadFunc {
 
 	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(pos, path, l.cfg.StdRoot)
-				}
-				return nil
+		impPath := importPath(path)
+		if isLocalImport(path) {
+			return cfg.newErrInstance(pos, impPath,
+				errors.Newf(pos, "relative import paths not allowed (%q)", path))
+		}
+
+		// is it a builtin?
+		if strings.IndexByte(strings.Split(path, "/")[0], '.') == -1 {
+			if l.cfg.StdRoot != "" {
+				p := cfg.newInstance(pos, impPath)
+				return l.importPkg(pos, p)
 			}
-			if cfg.ModuleRoot == "" {
-				i := cfg.newInstance(path)
-				report(i, l.errPkgf(nil,
-					"import %q not found in the pkg directory", path))
-				return i
-			}
-			root := cfg.ModuleRoot
-			if mod := cfg.Module; path == mod || strings.HasPrefix(path, mod+"/") {
-				path = path[len(mod)+1:]
-			} else {
-				root = filepath.Join(root, "pkg")
-			}
-			return l.importPkg(pos, path, root)
+			return nil
 		}
 
-		if strings.Contains(path, "@") {
-			i := cfg.newInstance(path)
-			report(i, l.errPkgf(nil,
-				"can only use path@version syntax with 'cue get'"))
-			return i
-		}
-
-		return l.importPkg(pos, path, parentPath)
+		p := cfg.newInstance(pos, impPath)
+		return l.importPkg(pos, p)
 	}
 }
 
-func updateDirs(c *Config, p *build.Instance, path, srcDir string, mode importMode) errors.Error {
-	p.DisplayPath = path
-	p.Root = c.ModuleRoot
-	p.Module = c.Module
-
-	isLocal := isLocalImport(path)
-	p.Local = isLocal
-
-	ctxt := &c.fileSystem
-
-	if path == "" {
-		return errors.Newf(token.NoPos, "import %q: invalid import path", path)
-	}
-
-	if ctxt.isAbsPath(path) || strings.HasPrefix(path, "/") {
-		return errors.Newf(token.NoPos, "absolute import path %q not allowed", path)
-	}
-
-	if isLocal {
-		if c.Module != "" {
-			p.ImportPath = filepath.Join(c.Module, path)
-		}
-
-		if srcDir == "" {
-			return errors.Newf(token.NoPos, "import %q: import relative to unknown directory", path)
-		}
-		p.Dir = ctxt.joinPath(srcDir, path)
-		return nil
-	}
-	dir := ctxt.joinPath(srcDir, path)
-	info, err := ctxt.stat(dir)
-	if err == nil && info.IsDir() {
-		p.Dir = dir
-		return nil
-	}
-
-	// package was not found
-	return errors.Newf(token.NoPos, "cannot find package %q", path)
-}
-
 func normPrefix(root, path string, isLocal bool) string {
 	root = filepath.Clean(root)
 	prefix := ""
@@ -366,16 +318,7 @@
 		return false // don't mark as added
 	}
 
-	if fp.c.Package != "" {
-		if pkg != fp.c.Package {
-			if fp.ignoreOther {
-				p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
-				return false
-			}
-			// TODO: package does not conform with requested.
-			return badFile(errors.Newf(pos, "%s: found package %q; want %q", filename, pkg, fp.c.Package))
-		}
-	} else if fp.firstFile == "" {
+	if p.PkgName == "" {
 		p.PkgName = pkg
 		fp.firstFile = name
 	} else if pkg != p.PkgName {
diff --git a/cue/load/import_test.go b/cue/load/import_test.go
index 26caedc..1b7d570 100644
--- a/cue/load/import_test.go
+++ b/cue/load/import_test.go
@@ -16,47 +16,49 @@
 
 import (
 	"os"
-	"path/filepath"
 	"reflect"
 	"testing"
 
 	build "cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/token"
+	"golang.org/x/xerrors"
 )
 
 const testdata = "./testdata/"
 
 func getInst(pkg, cwd string) (*build.Instance, error) {
-	c, _ := (&Config{}).complete()
+	c, _ := (&Config{Dir: cwd}).complete()
 	l := loader{cfg: c}
-	p := l.importPkg(token.NoPos, pkg, cwd)
+	inst := c.newRelInstance(token.NoPos, pkg)
+	p := l.importPkg(token.NoPos, inst)
 	return p, p.Err
 }
 
-func TestDotSlashImport(t *testing.T) {
-	c, _ := (&Config{}).complete()
-	l := loader{cfg: c}
-	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)
-	}
+// Uncomment this test if we decide to allow relative imports again.
+// func TestDotSlashImport(t *testing.T) {
+// 	c, _ := (&Config{}).complete()
+// 	l := loader{cfg: c}
+// 	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)
+// 	}
 
-	p1, err := getInst("./file", testdata+"other")
-	if err != nil {
-		t.Fatal(err)
-	}
-	if p1.PkgName != "file" {
-		t.Fatalf("./file: Name=%q, want %q", p1.PkgName, "file")
-	}
-	dir := filepath.Clean(testdata + "other/file") // Clean to use \ on Windows
-	if p1.Dir != dir {
-		t.Fatalf("./file: Dir=%q, want %q", p1.PkgName, dir)
-	}
-}
+// 	p1, err := getInst("./file", testdata+"other")
+// 	if err != nil {
+// 		t.Fatal(err)
+// 	}
+// 	if p1.PkgName != "file" {
+// 		t.Fatalf("./file: Name=%q, want %q", p1.PkgName, "file")
+// 	}
+// 	dir := filepath.Clean(testdata + "other/file") // Clean to use \ on Windows
+// 	if p1.Dir != dir {
+// 		t.Fatalf("./file: Dir=%q, want %q", p1.PkgName, dir)
+// 	}
+// }
 
 func TestEmptyImport(t *testing.T) {
 	p, err := getInst("", "")
@@ -80,7 +82,8 @@
 
 func TestIgnoredCUEFilesImport(t *testing.T) {
 	_, err := getInst(".", testdata+"ignored")
-	e, ok := err.(*noCUEError)
+	var e *noCUEError
+	ok := xerrors.As(err, &e)
 	if !ok {
 		t.Fatal(`Import("testdata/ignored") did not return NoCUEError.`)
 	}
@@ -95,8 +98,8 @@
 	if !ok {
 		t.Fatal(`Import("testdata/multi") did not return MultiplePackageError.`)
 	}
+	mpe.Dir = ""
 	want := &multiplePackageError{
-		Dir:      filepath.FromSlash("testdata/multi"),
 		Packages: []string{"main", "test_package"},
 		Files:    []string{"file.cue", "file_appengine.cue"},
 	}
diff --git a/cue/load/loader.go b/cue/load/loader.go
index c831705..ed64913 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -42,7 +42,7 @@
 	}
 	newC, err := c.complete()
 	if err != nil {
-		return []*build.Instance{c.newErrInstance(nil, "", err)}
+		return []*build.Instance{c.newErrInstance(token.NoPos, "", err)}
 	}
 	c = newC
 
@@ -55,13 +55,10 @@
 		return []*build.Instance{l.cueFilesPackage(args)}
 	}
 
-	dummy := c.newInstance("user")
-	dummy.Local = true
-
 	a := []*build.Instance{}
 	for _, m := range l.importPaths(args) {
 		if m.Err != nil {
-			inst := c.newErrInstance(m, "", m.Err)
+			inst := c.newErrInstance(token.NoPos, "", m.Err)
 			a = append(a, inst)
 			continue
 		}
@@ -102,17 +99,17 @@
 	pos := token.NoPos
 	cfg := l.cfg
 	// ModInit() // TODO: support modules
-	pkg := l.cfg.Context.NewInstance(cfg.Dir, l.loadFunc(cfg.Dir))
+	pkg := l.cfg.Context.NewInstance(cfg.Dir, l.loadFunc())
 
 	for _, f := range files {
 		if cfg.isDir(f) {
-			return cfg.newErrInstance(nil, f,
+			return cfg.newErrInstance(token.NoPos, toImportPath(f),
 				errors.Newf(pos, "cannot mix files with directories %v", f))
 		}
 		ext := filepath.Ext(f)
 		enc := encoding.MapExtension(ext)
 		if enc == nil {
-			return cfg.newErrInstance(nil, f,
+			return cfg.newErrInstance(token.NoPos, toImportPath(f),
 				errors.Newf(pos, "unrecognized extension %q", ext))
 		}
 	}
@@ -126,11 +123,11 @@
 		}
 		fi, err := cfg.fileSystem.stat(path)
 		if err != nil {
-			return cfg.newErrInstance(nil, path,
+			return cfg.newErrInstance(pos, toImportPath(path),
 				errors.Wrapf(err, pos, "could not find dir %s", path))
 		}
 		if fi.IsDir() {
-			return cfg.newErrInstance(nil, path,
+			return cfg.newErrInstance(pos, toImportPath(path),
 				errors.Newf(pos, "%s is a directory, should be a CUE file", file))
 		}
 		fp.add(pos, cfg.Dir, file, allowAnonymous)
@@ -139,7 +136,7 @@
 	// TODO: ModImportFromFiles(files)
 	_, err := filepath.Abs(cfg.Dir)
 	if err != nil {
-		return cfg.newErrInstance(nil, cfg.Dir,
+		return cfg.newErrInstance(pos, toImportPath(cfg.Dir),
 			errors.Wrapf(err, pos, "could convert '%s' to absolute path", cfg.Dir))
 	}
 	pkg.Dir = cfg.Dir
diff --git a/cue/load/loader_test.go b/cue/load/loader_test.go
index 26625f6..e9150d0 100644
--- a/cue/load/loader_test.go
+++ b/cue/load/loader_test.go
@@ -55,6 +55,7 @@
 module: example.org/test
 root:   $CWD/testdata
 dir:    $CWD/testdata
+display:.
 files:
     $CWD/testdata/test.cue
 imports:
@@ -70,33 +71,23 @@
 module: example.org/test
 root:   $CWD/testdata
 dir:    $CWD/testdata
+display:.
 files:
     $CWD/testdata/test.cue
 imports:
     example.org/test/sub: $CWD/testdata/sub/sub.cue`,
 	}, {
 		// TODO:
-		// - paths are incorrect, should be example.org/test/other:main and
-		//   example.org/test/other/file, respectively.
-		// - referenced import path of files is wrong.
+		// - path incorrect, should be example.org/test/other:main.
 		cfg:  dirCfg,
 		args: args("./other/..."),
 		want: `
-path:   example.org/test
+err:    relative import paths not allowed ("./file")
+path:   ""
 module: example.org/test
-root:   $CWD/testdata/
-dir:    $CWD/testdata/other
-files:
-	$CWD/testdata/other/main.cue
-imports:
-	./file: $CWD/testdata/other/file/file.cue
-
-path:   example.org/test/file
-module: example.org/test
-root:   $CWD/testdata/
-dir:    $CWD/testdata/other/file
-files:
-	$CWD/testdata/other/file/file.cue`,
+root:   $CWD/testdata
+dir:    $CWD/testdata/pkg
+display:`,
 	}, {
 		cfg:  dirCfg,
 		args: args("./anon"),
@@ -105,38 +96,67 @@
 path:   example.org/test/anon
 module: example.org/test
 root:   $CWD/testdata
-dir:    $CWD/testdata/anon`,
+dir:    $CWD/testdata/anon
+display:./anon`,
 	}, {
 		// TODO:
-		// - paths are incorrect, should be example.org/test/other:main and
-		//   example.org/test/other/file, respectively.
+		// - paths are incorrect, should be example.org/test/other:main.
 		cfg:  dirCfg,
 		args: args("./other"),
 		want: `
-path:   example.org/test/other
+err:    relative import paths not allowed ("./file")
+path:   example.org/test/other:main
 module: example.org/test
 root:   $CWD/testdata
 dir:    $CWD/testdata/other
+display:./other
 files:
-	$CWD/testdata/other/main.cue
-imports:
-	./file: $CWD/testdata/other/file/file.cue`,
+	$CWD/testdata/other/main.cue`,
 	}, {
 		// TODO:
 		// - incorrect path, should be example.org/test/hello:test
 		cfg:  dirCfg,
 		args: args("./hello"),
 		want: `
-path:   example.org/test/hello
+path:   example.org/test/hello:test
 module: example.org/test
 root:   $CWD/testdata
 dir:    $CWD/testdata/hello
+display:./hello
 files:
 	$CWD/testdata/test.cue
 	$CWD/testdata/hello/test.cue
 imports:
 	example.org/test/sub: $CWD/testdata/sub/sub.cue`,
 	}, {
+		// TODO:
+		// - incorrect path, should be example.org/test/hello:test
+		cfg:  dirCfg,
+		args: args("example.org/test/hello:test"),
+		want: `
+path:   example.org/test/hello:test
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/hello
+display:example.org/test/hello:test
+files:
+	$CWD/testdata/test.cue
+	$CWD/testdata/hello/test.cue
+imports:
+	example.org/test/sub: $CWD/testdata/sub/sub.cue`,
+	}, {
+		// TODO:
+		// - incorrect path, should be example.org/test/hello:test
+		cfg:  dirCfg,
+		args: args("example.org/test/hello:nonexist"),
+		want: `
+err:    build constraints exclude all CUE files in example.org/test/hello:nonexist
+path:   example.org/test/hello:nonexist
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/hello
+display:example.org/test/hello:nonexist`,
+	}, {
 		cfg:  dirCfg,
 		args: args("./anon.cue", "./other/anon.cue"),
 		want: `
@@ -144,6 +164,7 @@
 module: ""
 root:   $CWD/testdata
 dir:    $CWD/testdata
+display:command-line-arguments
 files:
 	$CWD/testdata/anon.cue
 	$CWD/testdata/other/anon.cue`,
@@ -156,6 +177,7 @@
 module: ""
 root:   $CWD/testdata
 dir:    $CWD/testdata
+display:command-line-arguments
 files:
 	$CWD/testdata/anon.cue`,
 	}, {
@@ -164,10 +186,11 @@
 		args: args("non-existing"),
 		want: `
 err:    cannot find package "non-existing"
-path:   ""
+path:   non-existing
 module: example.org/test
 root:   $CWD/testdata
-dir:    non-existing `,
+dir:    $CWD/testdata/pkg/non-existing
+display:non-existing`,
 	}, {
 		cfg:  dirCfg,
 		args: args("./empty"),
@@ -176,7 +199,8 @@
 path:   example.org/test/empty
 module: example.org/test
 root:   $CWD/testdata
-dir:    $CWD/testdata/empty`,
+dir:    $CWD/testdata/empty
+display:./empty`,
 	}, {
 		cfg:  dirCfg,
 		args: args("./imports"),
@@ -185,13 +209,17 @@
 module: example.org/test
 root:   $CWD/testdata
 dir:    $CWD/testdata/imports
+display:./imports
 files:
 	$CWD/testdata/imports/imports.cue
 imports:
 	acme.com/catch: $CWD/testdata/pkg/acme.com/catch/catch.cue
-	acme.com/helper: $CWD/testdata/pkg/acme.com/helper/helper.cue`,
+	acme.com/helper:helper1: $CWD/testdata/pkg/acme.com/helper/helper1.cue`,
 	}}
 	for i, tc := range testCases {
+		// if i != 5 {
+		// 	continue
+		// }
 		t.Run(strconv.Itoa(i)+"/"+strings.Join(tc.args, ":"), func(t *testing.T) {
 			pkgs := Instances(tc.args, tc.cfg)
 
@@ -223,6 +251,7 @@
 module: {{if .Module}}{{.Module}}{{else}}""{{end}}
 root:   {{.Root}}
 dir:    {{.Dir}}
+display:{{.DisplayPath}}
 {{if .Files -}}
 files:
 {{- range .Files}}
@@ -245,6 +274,9 @@
 	}
 	c := &Config{
 		Overlay: map[string]Source{
+			// Not necessary, but nice to add.
+			abs("cue.mod"): FromString(`module: acme.com`),
+
 			abs("dir/top.cue"): FromBytes([]byte(`
 			   package top
 			   msg: "Hello"
diff --git a/cue/load/search.go b/cue/load/search.go
index 3f2d0de..bd649d3 100644
--- a/cue/load/search.go
+++ b/cue/load/search.go
@@ -148,13 +148,14 @@
 
 	root := l.abs(dir)
 
-	if c.ModuleRoot != "" {
-		if !hasFilepathPrefix(root, c.ModuleRoot) {
-			m.Err = errors.Newf(token.NoPos,
-				"cue: pattern %s refers to dir %s, outside module root %s",
-				pattern, root, c.ModuleRoot)
-			return m
-		}
+	// Find new module root from here or check there are no additional
+	// cue.mod files between here and the next module.
+
+	if !hasFilepathPrefix(root, c.ModuleRoot) {
+		m.Err = errors.Newf(token.NoPos,
+			"cue: pattern %s refers to dir %s, outside module root %s",
+			pattern, root, c.ModuleRoot)
+		return m
 	}
 
 	pkgDir := filepath.Join(root, "pkg")
@@ -192,7 +193,17 @@
 		// 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(token.NoPos, "."+path[len(root):], root)
+		// Do not take root, as we want to stay relative
+		// to one dir only.
+		dir, e := filepath.Rel(c.Dir, path)
+		if e != nil {
+			panic(err)
+		} else {
+			dir = "./" + dir
+		}
+		// TODO: consider not doing these checks here.
+		inst := c.newRelInstance(token.NoPos, dir)
+		p := l.importPkg(token.NoPos, inst)
 		if err := p.Err; err != nil && (p == nil || len(p.InvalidCUEFiles) == 0) {
 			switch err.(type) {
 			case nil:
@@ -332,7 +343,14 @@
 			continue
 		}
 
-		pkg := l.importPkg(token.NoPos, a, l.cfg.Dir)
+		var p *build.Instance
+		if isLocalImport(a) {
+			p = l.cfg.newRelInstance(token.NoPos, a)
+		} else {
+			p = l.cfg.newInstance(token.NoPos, importPath(a))
+		}
+
+		pkg := l.importPkg(token.NoPos, p)
 		out = append(out, &match{Pattern: a, Literal: true, Pkgs: []*build.Instance{pkg}})
 	}
 	return out
diff --git a/cue/load/testdata/pkg/acme.com/catch/catch.cue b/cue/load/testdata/pkg/acme.com/catch/catch.cue
index 0b9beae..0cfbbe8 100644
--- a/cue/load/testdata/pkg/acme.com/catch/catch.cue
+++ b/cue/load/testdata/pkg/acme.com/catch/catch.cue
@@ -1,5 +1,5 @@
 package catch
 
-import "acme.com/helper"
+import "acme.com/helper:helper1"
 
 Method: "tnt" | "catapult" | "net" | helper.Gotcha
diff --git a/cue/load/testdata/pkg/acme.com/helper/helper1.cue b/cue/load/testdata/pkg/acme.com/helper/helper1.cue
new file mode 100644
index 0000000..62a8e6a
--- /dev/null
+++ b/cue/load/testdata/pkg/acme.com/helper/helper1.cue
@@ -0,0 +1,3 @@
+package helper1
+
+Gotcha: "gotcha"
diff --git a/cue/load/testdata/pkg/acme.com/helper/helper2.cue b/cue/load/testdata/pkg/acme.com/helper/helper2.cue
new file mode 100644
index 0000000..5c91907
--- /dev/null
+++ b/cue/load/testdata/pkg/acme.com/helper/helper2.cue
@@ -0,0 +1,3 @@
+package helper2
+
+Gotcha: "gotcha"
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 43ef86e..840be6d 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -1221,6 +1221,9 @@
 func isValidImport(lit string) bool {
 	const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
 	s, _ := literal.Unquote(lit) // go/scanner returns a legal string literal
+	if p := strings.LastIndexByte(s, ':'); p >= 0 {
+		s = s[:p]
+	}
 	for _, r := range s {
 		if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
 			return false