pkg/path: activate OS-dependent version

This also fixes a bad interaction between validators
and variable arguments: IsAbs qualifies as a validator,
but it is ambiguous to allow both. In the futurre we
could introduce an AbsDir function, or alike, that
functions only as a validator. Also `must` would help
here.

The fix has been left in this CL, as it isn't a problem
before this CL and it makes the context clear.

Overall, this doesn't seem to be too big of a deal.

Change-Id: I4f28ad507cd4d8a42c3d02bde28be53b7db3ad63
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7846
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/core/adt/expr.go b/internal/core/adt/expr.go
index 939ef6e..44891e0 100644
--- a/internal/core/adt/expr.go
+++ b/internal/core/adt/expr.go
@@ -980,10 +980,12 @@
 	Value Value   // Could become Value later, using disjunctions for defaults.
 }
 
+// Kind returns the kind mask of this parameter.
 func (p Param) Kind() Kind {
 	return p.Value.Kind()
 }
 
+// Default reports the default value for this Param or nil if there is none.
 func (p Param) Default() Value {
 	d, ok := p.Value.(*Disjunction)
 	if !ok || d.NumDefaults != 1 {
@@ -1009,8 +1011,12 @@
 	return &BuiltinValidator{Builtin: x}
 }
 
-func (x *Builtin) IsValidator(numArgs int) bool {
-	return len(x.Params)-1 == numArgs && x.Result&^BoolKind == 0
+// IsValidator reports whether b should be interpreted as a Validator for the
+// given number of arguments.
+func (b *Builtin) IsValidator(numArgs int) bool {
+	return numArgs == len(b.Params)-1 &&
+		b.Result&^BoolKind == 0 &&
+		b.Params[numArgs].Default() == nil
 }
 
 func bottom(v Value) *Bottom {
@@ -1040,21 +1046,35 @@
 		args = append(args, v)
 	}
 	for i, a := range args {
-		if x.Params[i].Kind() != BottomKind {
-			if b := bottom(a); b != nil {
-				return b
+		if x.Params[i].Kind() == BottomKind {
+			continue
+		}
+		if b := bottom(a); b != nil {
+			return b
+		}
+		if k := kind(a); x.Params[i].Kind()&k == BottomKind {
+			code := EvalError
+			b, _ := args[i].(*Bottom)
+			if b != nil {
+				code = b.Code
 			}
-			if k := kind(a); x.Params[i].Kind()&k == BottomKind {
-				code := EvalError
-				b, _ := args[i].(*Bottom)
-				if b != nil {
-					code = b.Code
-				}
-				c.addErrf(code, pos(a),
-					"cannot use %s (type %s) as %s in argument %d to %s",
-					a, k, x.Params[i].Kind(), i+1, fun)
-				return nil
+			c.addErrf(code, pos(a),
+				"cannot use %s (type %s) as %s in argument %d to %s",
+				a, k, x.Params[i].Kind(), i+1, fun)
+			return nil
+		}
+		v := x.Params[i].Value
+		if _, ok := v.(*BasicType); !ok {
+			env := c.Env(0)
+			x := &BinaryExpr{Op: AndOp, X: v, Y: a}
+			n := &Vertex{Conjuncts: []Conjunct{{env, x, 0}}}
+			c.Unifier.Unify(c, n, Finalized)
+			if _, ok := n.BaseValue.(*Bottom); ok {
+				c.addErrf(0, pos(a),
+					"cannot use %s as %s in argument %d to %s",
+					a, v, i+1, fun)
 			}
+			args[i] = n
 		}
 	}
 	return x.Func(c, args)
diff --git a/pkg/internal/builtin.go b/pkg/internal/builtin.go
index bbd0ccb..ab23868 100644
--- a/pkg/internal/builtin.go
+++ b/pkg/internal/builtin.go
@@ -55,9 +55,8 @@
 }
 
 type Param struct {
-	Kind    adt.Kind
-	Value   adt.Value // input constraint
-	Default adt.Value // may be nil
+	Kind  adt.Kind
+	Value adt.Value // input constraint (may be nil)
 }
 
 type Package struct {
@@ -112,16 +111,9 @@
 func toBuiltin(ctx *adt.OpContext, b *Builtin) *adt.Builtin {
 	params := make([]adt.Param, len(b.Params))
 	for i, p := range b.Params {
-		// TODO: use Value.
-		params[i].Value = &adt.BasicType{K: p.Kind}
-		if p.Default != nil {
-			params[i].Value = &adt.Disjunction{
-				NumDefaults: 1,
-				Values: []*adt.Vertex{
-					adt.ToVertex(p.Default),
-					adt.ToVertex(params[i].Value),
-				},
-			}
+		params[i].Value = p.Value
+		if params[i].Value == nil {
+			params[i].Value = &adt.BasicType{K: p.Kind}
 		}
 	}
 
diff --git a/pkg/path/testdata/example_nix_test.go b/pkg/path/example_nix_test.go
similarity index 100%
rename from pkg/path/testdata/example_nix_test.go
rename to pkg/path/example_nix_test.go
diff --git a/pkg/path/testdata/example_test.go b/pkg/path/example_test.go
similarity index 100%
rename from pkg/path/testdata/example_test.go
rename to pkg/path/example_test.go
diff --git a/pkg/path/manual.go b/pkg/path/manual.go
deleted file mode 100644
index 99525a7..0000000
--- a/pkg/path/manual.go
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2018 The CUE Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package path
-
-import "path"
-
-var split = path.Split
-
-// Split splits path immediately following the final slash and returns them as
-// the list [dir, file], separating it into a directory and file name component.
-// If there is no slash in path, Split returns an empty dir and file set to
-// path. The returned values have the property that path = dir+file.
-func Split(path string) []string {
-	file, dir := split(path)
-	return []string{file, dir}
-}
diff --git a/pkg/path/testdata/match.go b/pkg/path/match.go
similarity index 100%
rename from pkg/path/testdata/match.go
rename to pkg/path/match.go
diff --git a/pkg/path/testdata/match_test.go b/pkg/path/match_test.go
similarity index 100%
rename from pkg/path/testdata/match_test.go
rename to pkg/path/match_test.go
diff --git a/pkg/path/testdata/os.go b/pkg/path/os.go
similarity index 96%
rename from pkg/path/testdata/os.go
rename to pkg/path/os.go
index d5de0be..08592c3 100644
--- a/pkg/path/testdata/os.go
+++ b/pkg/path/os.go
@@ -14,6 +14,7 @@
 
 package path
 
+// OS must be a valid runtime.GOOS value or "unix".
 type OS string
 
 const (
diff --git a/pkg/path/path.go b/pkg/path/path.go
index bd82f26..9f29dd2 100644
--- a/pkg/path/path.go
+++ b/pkg/path/path.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The CUE Authors
+// Copyright 2020 CUE Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,54 +12,80 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Copyright 2018 The Go Authors. All rights reserved.
+// Copyright 2009 The Go Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:generate go run cuelang.org/go/internal/cmd/qgo -exclude=Split,Join extract path
-
+// Package filepath implements utility routines for manipulating filename paths
+// in a way compatible with the target operating system-defined file paths.
+//
+// The filepath package uses either forward slashes or backslashes,
+// depending on the operating system. To process paths such as URLs
+// that always use forward slashes regardless of the operating
+// system, see the path package.
 package path
 
-import "path"
+import (
+	"errors"
+	"strings"
+)
 
-// Match reports whether name matches the shell pattern.
-// The pattern syntax is:
-//
-//	pattern:
-//		{ term }
-//	term:
-//		'*'         matches any sequence of non-/ characters
-//		'?'         matches any single non-/ character
-//		'[' [ '^' ] { character-range } ']'
-//		            character class (must be non-empty)
-//		c           matches character c (c != '*', '?', '\\', '[')
-//		'\\' c      matches character c
-//
-//	character-range:
-//		c           matches character c (c != '\\', '-', ']')
-//		'\\' c      matches character c
-//		lo '-' hi   matches character c for lo <= c <= hi
-//
-// Match requires pattern to match all of name, not just a substring.
-// The only possible returned error is ErrBadPattern, when pattern
-// is malformed.
-//
-func Match(pattern, name string) (matched bool, err error) {
-	return path.Match(pattern, name)
+// A lazybuf is a lazily constructed path buffer.
+// It supports append, reading previously appended bytes,
+// and retrieving the final string. It does not allocate a buffer
+// to hold the output until that output diverges.
+type lazybuf struct {
+	path       string
+	buf        []byte
+	w          int
+	volAndPath string
+	volLen     int
+}
+
+func (b *lazybuf) index(i int) byte {
+	if b.buf != nil {
+		return b.buf[i]
+	}
+	return b.path[i]
+}
+
+func (b *lazybuf) append(c byte) {
+	if b.buf == nil {
+		if b.w < len(b.path) && b.path[b.w] == c {
+			b.w++
+			return
+		}
+		b.buf = make([]byte, len(b.path))
+		copy(b.buf, b.path[:b.w])
+	}
+	b.buf[b.w] = c
+	b.w++
+}
+
+func (b *lazybuf) string() string {
+	if b.buf == nil {
+		return b.volAndPath[:b.volLen+b.w]
+	}
+	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
 }
 
 // Clean returns the shortest path name equivalent to path
-// by purely lexical processing. It applies the following rules
+// by purely lexical processing. The default value for os is Unix.
+// It applies the following rules
 // iteratively until no further processing can be done:
 //
-//	1. Replace multiple slashes with a single slash.
+//	1. Replace multiple Separator elements with a single one.
 //	2. Eliminate each . path name element (the current directory).
 //	3. Eliminate each inner .. path name element (the parent directory)
 //	   along with the non-.. element that precedes it.
 //	4. Eliminate .. elements that begin a rooted path:
-//	   that is, replace "/.." by "/" at the beginning of a path.
+//	   that is, replace "/.." by "/" at the beginning of a path,
+//	   assuming Separator is '/'.
 //
-// The returned path ends in a slash only if it is the root "/".
+// The returned path ends in a slash only if it represents a root directory,
+// such as "/" on Unix or `C:\` on Windows.
+//
+// Finally, any occurrences of slash are replaced by Separator.
 //
 // If the result of this process is an empty string, Clean
 // returns the string ".".
@@ -67,38 +93,319 @@
 // See also Rob Pike, ``Lexical File Names in Plan 9 or
 // Getting Dot-Dot Right,''
 // https://9p.io/sys/doc/lexnames.html
-func Clean(path string) string { return pathClean(path) }
+func Clean(path string, os OS) string {
+	return clean(path, getOS(os))
+}
 
-var pathClean = path.Clean
+func clean(path string, os os) string {
+	originalPath := path
+	volLen := os.volumeNameLen(path)
+	path = path[volLen:]
+	if path == "" {
+		if volLen > 1 && originalPath[1] != ':' {
+			// should be UNC
+			return fromSlash(originalPath, os)
+		}
+		return originalPath + "."
+	}
+	rooted := os.IsPathSeparator(path[0])
+
+	// Invariants:
+	//	reading from path; r is index of next byte to process.
+	//	writing to buf; w is index of next byte to write.
+	//	dotdot is index in buf where .. must stop, either because
+	//		it is the leading slash or it is a leading ../../.. prefix.
+	n := len(path)
+	out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
+	r, dotdot := 0, 0
+	if rooted {
+		out.append(os.Separator)
+		r, dotdot = 1, 1
+	}
+
+	for r < n {
+		switch {
+		case os.IsPathSeparator(path[r]):
+			// empty path element
+			r++
+		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
+			// . element
+			r++
+		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
+			// .. element: remove to last separator
+			r += 2
+			switch {
+			case out.w > dotdot:
+				// can backtrack
+				out.w--
+				for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) {
+					out.w--
+				}
+			case !rooted:
+				// cannot backtrack, but not rooted, so append .. element.
+				if out.w > 0 {
+					out.append(os.Separator)
+				}
+				out.append('.')
+				out.append('.')
+				dotdot = out.w
+			}
+		default:
+			// real path element.
+			// add slash if needed
+			if rooted && out.w != 1 || !rooted && out.w != 0 {
+				out.append(os.Separator)
+			}
+			// copy element
+			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
+				out.append(path[r])
+			}
+		}
+	}
+
+	// Turn empty string into "."
+	if out.w == 0 {
+		out.append('.')
+	}
+
+	return fromSlash(out.string(), os)
+}
+
+// ToSlash returns the result of replacing each separator character
+// in path with a slash ('/') character. Multiple separators are
+// replaced by multiple slashes.
+func ToSlash(path string, os OS) string {
+	return toSlash(path, getOS(os))
+}
+
+func toSlash(path string, os os) string {
+	if os.Separator == '/' {
+		return path
+	}
+	return strings.ReplaceAll(path, string(os.Separator), "/")
+}
+
+// FromSlash returns the result of replacing each slash ('/') character
+// in path with a separator character. Multiple slashes are replaced
+// by multiple separators.
+func FromSlash(path string, os OS) string {
+	return fromSlash(path, getOS(os))
+}
+
+func fromSlash(path string, os os) string {
+	if os.Separator == '/' {
+		return path
+	}
+	return strings.ReplaceAll(path, "/", string(os.Separator))
+}
+
+// SplitList splits a list of paths joined by the OS-specific ListSeparator,
+// usually found in PATH or GOPATH environment variables.
+// Unlike strings.Split, SplitList returns an empty slice when passed an empty
+// string.
+func SplitList(path string, os OS) []string {
+	return getOS(os).splitList(path)
+}
+
+// Split splits path immediately following the final slash and returns them as
+// the list [dir, file], separating it into a directory and file name component.
+// If there is no slash in path, Split returns an empty dir and file set to
+// path. The returned values have the property that path = dir+file.
+// The default value for os is Unix.
+func Split(path string, os OS) []string {
+	x := getOS(os)
+	vol := volumeName(path, x)
+	i := len(path) - 1
+	for i >= len(vol) && !x.IsPathSeparator(path[i]) {
+		i--
+	}
+	return []string{path[:i+1], path[i+1:]}
+}
+
+// Join joins any number of path elements into a single path,
+// separating them with an OS specific Separator. Empty elements
+// are ignored. The result is Cleaned. However, if the argument
+// list is empty or all its elements are empty, Join returns
+// an empty string.
+// On Windows, the result will only be a UNC path if the first
+// non-empty element is a UNC path.
+// The default value for os is Unix.
+func Join(elem []string, os OS) string {
+	return getOS(os).join(elem)
+}
 
 // Ext returns the file name extension used by path.
 // The extension is the suffix beginning at the final dot
-// in the final slash-separated element of path;
-// it is empty if there is no dot.
-func Ext(path string) string { return pathExt(path) }
+// in the final element of path; it is empty if there is
+// no dot. The default value for os is Unix.
+func Ext(path string, os OS) string {
+	x := getOS(os)
+	for i := len(path) - 1; i >= 0 && !x.IsPathSeparator(path[i]); i-- {
+		if path[i] == '.' {
+			return path[i:]
+		}
+	}
+	return ""
+}
 
-var pathExt = path.Ext
+// Resolve reports the path of sub relative to dir. If sub is an absolute path,
+// or if dir is empty, it will return sub. If sub is empty, it will return dir.
+// Resolve calls Clean on the result. The default value for os is Unix.
+func Resolve(dir, sub string, os OS) string {
+	x := getOS(os)
+	if x.IsAbs(sub) {
+		return clean(sub, x)
+	}
+	dir = clean(dir, x)
+	return x.join([]string{dir, sub})
+}
+
+// Rel returns a relative path that is lexically equivalent to targpath when
+// joined to basepath with an intervening separator. That is,
+// Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself.
+// On success, the returned path will always be relative to basepath,
+// even if basepath and targpath share no elements.
+// An error is returned if targpath can't be made relative to basepath or if
+// knowing the current working directory would be necessary to compute it.
+// Rel calls Clean on the result. The default value for os is Unix.
+func Rel(basepath, targpath string, os OS) (string, error) {
+	x := getOS(os)
+	baseVol := volumeName(basepath, x)
+	targVol := volumeName(targpath, x)
+	base := clean(basepath, x)
+	targ := clean(targpath, x)
+	if x.sameWord(targ, base) {
+		return ".", nil
+	}
+	base = base[len(baseVol):]
+	targ = targ[len(targVol):]
+	if base == "." {
+		base = ""
+	}
+	// Can't use IsAbs - `\a` and `a` are both relative in Windows.
+	baseSlashed := len(base) > 0 && base[0] == x.Separator
+	targSlashed := len(targ) > 0 && targ[0] == x.Separator
+	if baseSlashed != targSlashed || !x.sameWord(baseVol, targVol) {
+		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
+	}
+	// Position base[b0:bi] and targ[t0:ti] at the first differing elements.
+	bl := len(base)
+	tl := len(targ)
+	var b0, bi, t0, ti int
+	for {
+		for bi < bl && base[bi] != x.Separator {
+			bi++
+		}
+		for ti < tl && targ[ti] != x.Separator {
+			ti++
+		}
+		if !x.sameWord(targ[t0:ti], base[b0:bi]) {
+			break
+		}
+		if bi < bl {
+			bi++
+		}
+		if ti < tl {
+			ti++
+		}
+		b0 = bi
+		t0 = ti
+	}
+	if base[b0:bi] == ".." {
+		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
+	}
+	if b0 != bl {
+		// Base elements left. Must go up before going down.
+		seps := strings.Count(base[b0:bl], string(x.Separator))
+		size := 2 + seps*3
+		if tl != t0 {
+			size += 1 + tl - t0
+		}
+		buf := make([]byte, size)
+		n := copy(buf, "..")
+		for i := 0; i < seps; i++ {
+			buf[n] = x.Separator
+			copy(buf[n+1:], "..")
+			n += 3
+		}
+		if t0 != tl {
+			buf[n] = x.Separator
+			copy(buf[n+1:], targ[t0:])
+		}
+		return string(buf), nil
+	}
+	return targ[t0:], nil
+}
 
 // Base returns the last element of path.
-// Trailing slashes are removed before extracting the last element.
+// Trailing path separators are removed before extracting the last element.
 // If the path is empty, Base returns ".".
-// If the path consists entirely of slashes, Base returns "/".
-func Base(path string) string { return pathBase(path) }
-
-var pathBase = path.Base
-
-// IsAbs reports whether the path is absolute.
-func IsAbs(path string) bool { return pathIsAbs(path) }
-
-var pathIsAbs = path.IsAbs
+// If the path consists entirely of separators, Base returns a single separator.
+// The default value for os is Unix.
+func Base(path string, os OS) string {
+	x := getOS(os)
+	if path == "" {
+		return "."
+	}
+	// Strip trailing slashes.
+	for len(path) > 0 && x.IsPathSeparator(path[len(path)-1]) {
+		path = path[0 : len(path)-1]
+	}
+	// Throw away volume name
+	path = path[x.volumeNameLen(path):]
+	// Find the last element
+	i := len(path) - 1
+	for i >= 0 && !x.IsPathSeparator(path[i]) {
+		i--
+	}
+	if i >= 0 {
+		path = path[i+1:]
+	}
+	// If empty now, it had only slashes.
+	if path == "" {
+		return string(x.Separator)
+	}
+	return path
+}
 
 // Dir returns all but the last element of path, typically the path's directory.
-// After dropping the final element using Split, the path is Cleaned and trailing
+// After dropping the final element, Dir calls Clean on the path and trailing
 // slashes are removed.
 // If the path is empty, Dir returns ".".
-// If the path consists entirely of slashes followed by non-slash bytes, Dir
-// returns a single slash. In any other case, the returned path does not end in a
-// slash.
-func Dir(path string) string { return pathDir(path) }
+// If the path consists entirely of separators, Dir returns a single separator.
+// The returned path does not end in a separator unless it is the root directory.
+// The default value for os is Unix.
+func Dir(path string, os OS) string {
+	x := getOS(os)
+	vol := volumeName(path, x)
+	i := len(path) - 1
+	for i >= len(vol) && !x.IsPathSeparator(path[i]) {
+		i--
+	}
+	dir := clean(path[len(vol):i+1], x)
+	if dir == "." && len(vol) > 2 {
+		// must be UNC
+		return vol
+	}
+	return vol + dir
+}
 
-var pathDir = path.Dir
+// IsAbs reports whether the path is absolute. The default value for os is Unix.
+// Note that because IsAbs has a default value, it cannot be used as
+// a validator.
+func IsAbs(path string, os OS) bool {
+	return getOS(os).IsAbs(path)
+}
+
+// VolumeName returns leading volume name.
+// Given "C:\foo\bar" it returns "C:" on Windows.
+// Given "\\host\share\foo" it returns "\\host\share".
+// On other platforms it returns "".
+// The default value for os is Windows.
+func VolumeName(path string, os OS) string {
+	return volumeName(path, getOS(os))
+}
+
+func volumeName(path string, os os) string {
+	return path[:os.volumeNameLen(path)]
+}
diff --git a/pkg/path/testdata/path_nix.go b/pkg/path/path_nix.go
similarity index 100%
rename from pkg/path/testdata/path_nix.go
rename to pkg/path/path_nix.go
diff --git a/pkg/path/testdata/path_p9.go b/pkg/path/path_p9.go
similarity index 100%
rename from pkg/path/testdata/path_p9.go
rename to pkg/path/path_p9.go
diff --git a/pkg/path/testdata/path_test.go b/pkg/path/path_test.go
similarity index 100%
rename from pkg/path/testdata/path_test.go
rename to pkg/path/path_test.go
diff --git a/pkg/path/testdata/path_win.go b/pkg/path/path_win.go
similarity index 100%
rename from pkg/path/testdata/path_win.go
rename to pkg/path/path_win.go
diff --git a/pkg/path/testdata/path_windows_test.go b/pkg/path/path_windows_test.go
similarity index 100%
rename from pkg/path/testdata/path_windows_test.go
rename to pkg/path/path_windows_test.go
diff --git a/pkg/path/testdata/pathtxtar_test.go b/pkg/path/pathtxtar_test.go
similarity index 100%
rename from pkg/path/testdata/pathtxtar_test.go
rename to pkg/path/pathtxtar_test.go
diff --git a/pkg/path/pkg.go b/pkg/path/pkg.go
index 5446870..dbe0f74 100644
--- a/pkg/path/pkg.go
+++ b/pkg/path/pkg.go
@@ -1,7 +1,16 @@
-// Code generated by go generate. DO NOT EDIT.
-
-//go:generate rm pkg.go
-//go:generate go run ../gen/gen.go
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
 
 package path
 
@@ -16,90 +25,245 @@
 
 var _ = adt.TopKind // in case the adt package isn't used
 
+var (
+	osRequired = &adt.Disjunction{
+		Values: allOS,
+	}
+
+	unixDefault = &adt.Disjunction{
+		NumDefaults: 1,
+		Values:      allOS,
+	}
+
+	// windowsDefault is the default for VolumeName.
+	windowsDefault = &adt.Disjunction{
+		NumDefaults: 1,
+		Values: append([]*adt.Vertex{
+			newStr("windows"),
+			newStr("unix"),
+			newStr("plan9")}, unixOS...),
+	}
+
+	allOS = append([]*adt.Vertex{
+		newStr("unix"),
+		newStr("windows"),
+		newStr("plan9"),
+	}, unixOS...)
+
+	// These all fall back to unix
+	unixOS = []*adt.Vertex{
+		newStr("aix"),
+		newStr("android"),
+		newStr("darwin"),
+		newStr("dragonfly"),
+		newStr("freebsd"),
+		newStr("hurd"),
+		newStr("illumos"),
+		newStr("ios"),
+		newStr("js"),
+		newStr("linux"),
+		newStr("nacl"),
+		newStr("netbsd"),
+		newStr("openbsd"),
+		newStr("solaris"),
+		newStr("zos"),
+	}
+)
+
+func newStr(s string) *adt.Vertex {
+	v := &adt.Vertex{}
+	v.SetValue(nil, adt.Finalized, &adt.String{Str: s})
+	return v
+}
+
 var pkg = &internal.Package{
+	CUE: `{
+		Unix:    "unix"
+		Windows: "windows"
+		Plan9:   "plan9"
+	}`,
 	Native: []*internal.Builtin{{
 		Name: "Split",
 		Params: []internal.Param{
 			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
 		},
 		Result: adt.ListKind,
 		Func: func(c *internal.CallCtxt) {
-			path := c.String(0)
+			path, os := c.String(0), c.String(1)
 			if c.Do() {
-				c.Ret = Split(path)
+				c.Ret = Split(path, OS(os))
+			}
+		},
+	}, {
+		Name: "SplitList",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: osRequired},
+		},
+		Result: adt.ListKind,
+		Func: func(c *internal.CallCtxt) {
+			path, os := c.String(0), c.String(1)
+			if c.Do() {
+				c.Ret = SplitList(path, OS(os))
+			}
+		},
+	}, {
+		Name: "Join",
+		Params: []internal.Param{
+			{Kind: adt.ListKind},
+			{Kind: adt.StringKind, Value: unixDefault},
+		},
+		Result: adt.StringKind,
+		Func: func(c *internal.CallCtxt) {
+			list, os := c.StringList(0), c.String(1)
+			if c.Do() {
+				c.Ret = Join(list, OS(os))
 			}
 		},
 	}, {
 		Name: "Match",
 		Params: []internal.Param{
 			{Kind: adt.StringKind},
-			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
 		},
 		Result: adt.BoolKind,
 		Func: func(c *internal.CallCtxt) {
-			pattern, name := c.String(0), c.String(1)
+			pattern, name, os := c.String(0), c.String(1), c.String(2)
 			if c.Do() {
-				c.Ret, c.Err = Match(pattern, name)
+				c.Ret, c.Err = Match(pattern, name, OS(os))
 			}
 		},
 	}, {
 		Name: "Clean",
 		Params: []internal.Param{
 			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
 		},
 		Result: adt.StringKind,
 		Func: func(c *internal.CallCtxt) {
-			path := c.String(0)
+			path, os := c.String(0), c.String(1)
 			if c.Do() {
-				c.Ret = Clean(path)
+				c.Ret = Clean(path, OS(os))
+			}
+		},
+	}, {
+		Name: "ToSlash",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: osRequired},
+		},
+		Result: adt.StringKind,
+		Func: func(c *internal.CallCtxt) {
+			path, os := c.String(0), c.String(1)
+			if c.Do() {
+				c.Ret = ToSlash(path, OS(os))
+			}
+		},
+	}, {
+		Name: "FromSlash",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: osRequired},
+		},
+		Result: adt.StringKind,
+		Func: func(c *internal.CallCtxt) {
+			path, os := c.String(0), c.String(1)
+			if c.Do() {
+				c.Ret = FromSlash(path, OS(os))
 			}
 		},
 	}, {
 		Name: "Ext",
 		Params: []internal.Param{
 			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
 		},
 		Result: adt.StringKind,
 		Func: func(c *internal.CallCtxt) {
-			path := c.String(0)
+			path, os := c.String(0), c.String(1)
 			if c.Do() {
-				c.Ret = Ext(path)
+				c.Ret = Ext(path, OS(os))
+			}
+		},
+	}, {
+		Name: "Resolve",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
+		},
+		Result: adt.StringKind,
+		Func: func(c *internal.CallCtxt) {
+			dir, sub, os := c.String(0), c.String(1), c.String(2)
+			if c.Do() {
+				c.Ret = Resolve(dir, sub, OS(os))
+			}
+		},
+	}, {
+		Name: "Rel",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
+		},
+		Result: adt.StringKind,
+		Func: func(c *internal.CallCtxt) {
+			base, target, os := c.String(0), c.String(1), c.String(2)
+			if c.Do() {
+				c.Ret, c.Err = Rel(base, target, OS(os))
 			}
 		},
 	}, {
 		Name: "Base",
 		Params: []internal.Param{
 			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
 		},
 		Result: adt.StringKind,
 		Func: func(c *internal.CallCtxt) {
-			path := c.String(0)
+			path, os := c.String(0), c.String(1)
 			if c.Do() {
-				c.Ret = Base(path)
-			}
-		},
-	}, {
-		Name: "IsAbs",
-		Params: []internal.Param{
-			{Kind: adt.StringKind},
-		},
-		Result: adt.BoolKind,
-		Func: func(c *internal.CallCtxt) {
-			path := c.String(0)
-			if c.Do() {
-				c.Ret = IsAbs(path)
+				c.Ret = Base(path, OS(os))
 			}
 		},
 	}, {
 		Name: "Dir",
 		Params: []internal.Param{
 			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
 		},
 		Result: adt.StringKind,
 		Func: func(c *internal.CallCtxt) {
-			path := c.String(0)
+			path, os := c.String(0), c.String(1)
 			if c.Do() {
-				c.Ret = Dir(path)
+				c.Ret = Dir(path, OS(os))
+			}
+		},
+	}, {
+		Name: "IsAbs",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: unixDefault},
+		},
+		Result: adt.BoolKind,
+		Func: func(c *internal.CallCtxt) {
+			path, os := c.String(0), c.String(1)
+			if c.Do() {
+				c.Ret = IsAbs(path, OS(os))
+			}
+		},
+	}, {
+		Name: "VolumeName",
+		Params: []internal.Param{
+			{Kind: adt.StringKind},
+			{Kind: adt.StringKind, Value: windowsDefault},
+		},
+		Result: adt.StringKind,
+		Func: func(c *internal.CallCtxt) {
+			path, os := c.String(0), c.String(1)
+			if c.Do() {
+				c.Ret = VolumeName(path, OS(os))
 			}
 		},
 	}},
diff --git a/pkg/path/testdata/error.txtar b/pkg/path/testdata/error.txtar
new file mode 100644
index 0000000..f53048c
--- /dev/null
+++ b/pkg/path/testdata/error.txtar
@@ -0,0 +1,28 @@
+// Copyright 2020 CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+-- in.cue --
+import "path"
+
+joinOK: path.Join(["a", "b"], "aix")
+joinErr: path.Join(["a", "b"], "foo")
+-- out/path --
+Errors:
+joinErr: cannot use "foo" as *"unix" | "windows" | "plan9" | "aix" | "android" | "darwin" | "dragonfly" | "freebsd" | "hurd" | "illumos" | "ios" | "js" | "linux" | "nacl" | "netbsd" | "openbsd" | "solaris" | "zos" in argument 2 to path.Join:
+    ./in.cue:4:32
+
+Result:
+joinOK:  "a/b"
+joinErr: _|_ // joinErr: cannot use "foo" as *"unix" | "windows" | "plan9" | "aix" | "android" | "darwin" | "dragonfly" | "freebsd" | "hurd" | "illumos" | "ios" | "js" | "linux" | "nacl" | "netbsd" | "openbsd" | "solaris" | "zos" in argument 2 to path.Join (and 1 more errors)
+
diff --git a/pkg/path/testdata/join.txtar b/pkg/path/testdata/join.txtar
new file mode 100644
index 0000000..82b6b55
--- /dev/null
+++ b/pkg/path/testdata/join.txtar
@@ -0,0 +1,205 @@
+// Copyright 2020 CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+-- in.cue --
+import "path"
+
+joinSingle: path.Join(["a", "b"])
+joinSingle: "a/b"
+
+Join: unix:    _
+Join: windows: _
+
+Join: [OS=string]: [...{
+    arg: [...string]
+
+    out: path.Join(arg, OS)
+}]
+
+Join: [_]: [
+    {arg: ["a", "b"]},
+    {arg: ["a/b", "c/d"]},
+
+    {arg: ["/"]},
+    {arg: ["a"]},
+
+    {arg: ["a", "b"]},
+    {arg: ["a", ""]},
+    {arg: ["", "b"]},
+    {arg: ["/", "a"]},
+    {arg: ["/", "a/b"]},
+    {arg: ["/", ""]},
+    {arg: ["//", "a"]},
+
+    {arg: ["directory", "file"]},
+
+    {arg: [#"C:\Windows\"#, #"System32"#]},
+    {arg: [#"C:\Windows\"#, #""#]},
+    {arg: [#"C:\"#, #"Windows"#]},
+    {arg: [#"C:"#, #"a\b"#]},
+    {arg: [#"C:"#, #"a"#, #"b"#]},
+    {arg: [#"C:"#, #""#, #""#, #"b"#]},
+    {arg: [#"C:"#, #""#]},
+    {arg: [#"C:"#, #""#, #""#]},
+    {arg: [#"C:."#, #"a"#]},
+    {arg: [#"C:a"#, #"b"#]},
+    {arg: [#"\\host\share"#, #"foo"#]},
+]
+
+-- out/path --
+joinSingle: "a/b"
+Join: {
+	unix: [{
+		arg: ["a", "b"]
+		out: "a/b"
+	}, {
+		arg: ["a/b", "c/d"]
+		out: "a/b/c/d"
+	}, {
+		arg: ["/"]
+		out: "/"
+	}, {
+		arg: ["a"]
+		out: "a"
+	}, {
+		arg: ["a", "b"]
+		out: "a/b"
+	}, {
+		arg: ["a", ""]
+		out: "a"
+	}, {
+		arg: ["", "b"]
+		out: "b"
+	}, {
+		arg: ["/", "a"]
+		out: "/a"
+	}, {
+		arg: ["/", "a/b"]
+		out: "/a/b"
+	}, {
+		arg: ["/", ""]
+		out: "/"
+	}, {
+		arg: ["//", "a"]
+		out: "/a"
+	}, {
+		arg: ["directory", "file"]
+		out: "directory/file"
+	}, {
+		arg: [#"C:\Windows\"#, #"System32"#]
+		out: "C:\\Windows\\/System32"
+	}, {
+		arg: [#"C:\Windows\"#, #""#]
+		out: "C:\\Windows\\"
+	}, {
+		arg: [#"C:\"#, #"Windows"#]
+		out: "C:\\/Windows"
+	}, {
+		arg: [#"C:"#, #"a\b"#]
+		out: "C:/a\\b"
+	}, {
+		arg: [#"C:"#, #"a"#, #"b"#]
+		out: "C:/a/b"
+	}, {
+		arg: [#"C:"#, #""#, #""#, #"b"#]
+		out: "C:/b"
+	}, {
+		arg: [#"C:"#, #""#]
+		out: "C:"
+	}, {
+		arg: [#"C:"#, #""#, #""#]
+		out: "C:"
+	}, {
+		arg: [#"C:."#, #"a"#]
+		out: "C:./a"
+	}, {
+		arg: [#"C:a"#, #"b"#]
+		out: "C:a/b"
+	}, {
+		arg: [#"\\host\share"#, #"foo"#]
+		out: "\\\\host\\share/foo"
+	}]
+	windows: [{
+		arg: ["a", "b"]
+		out: "a\\b"
+	}, {
+		arg: ["a/b", "c/d"]
+		out: "a\\b\\c\\d"
+	}, {
+		arg: ["/"]
+		out: "\\"
+	}, {
+		arg: ["a"]
+		out: "a"
+	}, {
+		arg: ["a", "b"]
+		out: "a\\b"
+	}, {
+		arg: ["a", ""]
+		out: "a"
+	}, {
+		arg: ["", "b"]
+		out: "b"
+	}, {
+		arg: ["/", "a"]
+		out: "\\a"
+	}, {
+		arg: ["/", "a/b"]
+		out: "\\a\\b"
+	}, {
+		arg: ["/", ""]
+		out: "\\"
+	}, {
+		arg: ["//", "a"]
+		out: "\\a"
+	}, {
+		arg: ["directory", "file"]
+		out: "directory\\file"
+	}, {
+		arg: [#"C:\Windows\"#, #"System32"#]
+		out: "C:\\Windows\\System32"
+	}, {
+		arg: [#"C:\Windows\"#, #""#]
+		out: "C:\\Windows"
+	}, {
+		arg: [#"C:\"#, #"Windows"#]
+		out: "C:\\Windows"
+	}, {
+		arg: [#"C:"#, #"a\b"#]
+		out: "C:a\\b"
+	}, {
+		arg: [#"C:"#, #"a"#, #"b"#]
+		out: "C:a\\b"
+	}, {
+		arg: [#"C:"#, #""#, #""#, #"b"#]
+		out: "C:b"
+	}, {
+		arg: [#"C:"#, #""#]
+		out: "C:."
+	}, {
+		arg: [#"C:"#, #""#, #""#]
+		out: "C:."
+	}, {
+		arg: [#"C:."#, #"a"#]
+		out: "C:a"
+	}, {
+		arg: [#"C:a"#, #"b"#]
+		out: "C:a\\b"
+	}, {
+		arg: [#"\\host\share"#, #"foo"#]
+		out: "\\\\host\\share\\foo"
+	}]
+}
+
diff --git a/pkg/path/testdata/os.txtar b/pkg/path/testdata/os.txtar
new file mode 100644
index 0000000..ba253da
--- /dev/null
+++ b/pkg/path/testdata/os.txtar
@@ -0,0 +1,504 @@
+// Copyright 2020 CUE Authors
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//     http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+-- in.cue --
+import "path"
+
+#OSes: [path.Unix, path.Windows, path.Plan9]
+#AnyOS: or(#OSes)
+
+// Test these OSes for all tests below.
+{
+    [string]: {
+        unix:    _
+        plan9:   _
+        windows: _
+    }
+}
+
+Split: [OS=#AnyOS]: [ARG=string]: path.Split(ARG, OS)
+Split: default:     [ARG=string]: path.Split(ARG)
+Split: default:     Split.unix
+Split: [_]: {
+    "/foo/bar/baz":       _
+    "a/b":                _
+    "//host/share/foo":   _
+    #"\\host\share\foo"#: _
+    "c:/foo/bar":         _
+    #"c:\foo\bar"#:       _
+}
+
+SplitList: [OS=string]: [ARG=string]: path.SplitList(ARG, OS)
+SplitList: [_]: {
+    "a:b":      _
+    "a\u0000b": _
+    "a;b":      _
+}
+
+Clean: [OS=#AnyOS]: [ARG=string]: path.Clean(ARG, OS)
+Clean: default:     [ARG=string]: path.Clean(ARG)
+Clean: default:     Clean.unix
+Clean: [_]: {
+    "abc//def//ghi":      _
+    #"c:\abc\def\..\.."#: _
+}
+
+Slash: [OS=string]: [ARG=string]: {
+    to:   path.ToSlash(ARG, OS)
+    from: path.FromSlash(ARG, OS)
+
+    // should roundtrip
+    to:   path.ToSlash(from, OS)
+    from: path.FromSlash(to, OS)
+}
+Slash: [_]: {
+    "":      _
+    "/":     _
+    "/a/b":  _
+    "/a//b": _
+}
+
+Ext: [OS=#AnyOS]: [ARG=string]: path.Ext(ARG, OS)
+Ext: default:     [ARG=string]: path.Ext(ARG)
+Ext: default:     Ext.unix
+Ext: [_]: {
+    // Same for all OS-es
+    "path.go":    ".go"
+    "path.pb.go": ".go"
+    "a.dir/b":    ""
+    "a.dir/b.go": ".go"
+    "a.dir/":     ""
+
+    // Differs on Windows.
+    "a.dir\\foo": _
+}
+
+Resolve: [OS=#AnyOS]: [A1=_]: [A2=_]: path.Resolve(A1, A2, OS)
+Resolve: default:     [A1=_]: [A2=_]: path.Resolve(A1, A2)
+Resolve: default:     Resolve.unix
+Resolve: [_]: {
+    "a/b/c": "d/e":   _
+    "/a/b":  "/c/d":  _
+    "c:/a":  #"d:\"#: _
+
+    "//home/user/foo": "bar":             _
+    "//home/user/foo": "//other/abs/foo": _
+}
+
+IsAbs: [OS=#AnyOS]: [ARG=string]: path.IsAbs(ARG, OS)
+IsAbs: default:     [ARG=string]: path.IsAbs(ARG)
+IsAbs: default:     IsAbs.unix
+IsAbs: [_]: {
+    "":     _
+    "/a":   _
+    "a":    _
+    "c:":   _
+    "c:/":  _
+    "c:\\": _
+
+    "//home/user/foo": _
+}
+
+
+Volume: [OS=string]:   [ARG=string]: path.VolumeName(ARG, OS)
+Volume: [!="windows"]: [string]:     "" // non-windows is always ""
+Volume: [_]: {
+    "c:/foo/bar": _
+    "c:":         _
+    "2:":         _
+    "":           _
+
+    #"\\\host"#:          _
+    #"\\\host\"#:         _
+    #"\\\host\share"#:    _
+    #"\\\host\\share"#:   _
+    #"\\host"#:           _
+    #"//host"#:           _
+    #"\\host\"#:          _
+    #"//host/"#:          _
+    #"\\host\share"#:     _
+    #"//host/share"#:     _
+    #"\\host\share\"#:    _
+    #"//host/share/"#:    _
+    #"\\host\share\foo"#: _
+    #"//host/share/foo"#: _
+
+    #"\\host\share\\foo\\\bar\\\\baz"#: _
+    #"//host/share//foo///bar////baz"#: _
+    #"\\host\share\foo\..\bar"#:        _
+    #"//host/share/foo/../bar"#:        _
+}
+
+-- out/path --
+#OSes: ["unix", "windows", "plan9"]
+#AnyOS: "unix" | "windows" | "plan9"
+Split: {
+	unix: {
+		"/foo/bar/baz": ["/foo/bar/", "baz"]
+		"a/b": ["a/", "b"]
+		"//host/share/foo": ["//host/share/", "foo"]
+		"\\\\host\\share\\foo": ["", "\\\\host\\share\\foo"]
+		"c:/foo/bar": ["c:/foo/", "bar"]
+		"c:\\foo\\bar": ["", "c:\\foo\\bar"]
+	}
+	plan9: {
+		"/foo/bar/baz": ["/foo/bar/", "baz"]
+		"a/b": ["a/", "b"]
+		"//host/share/foo": ["//host/share/", "foo"]
+		"\\\\host\\share\\foo": ["", "\\\\host\\share\\foo"]
+		"c:/foo/bar": ["c:/foo/", "bar"]
+		"c:\\foo\\bar": ["", "c:\\foo\\bar"]
+	}
+	default: {
+		"/foo/bar/baz": ["/foo/bar/", "baz"]
+		"a/b": ["a/", "b"]
+		"//host/share/foo": ["//host/share/", "foo"]
+		"\\\\host\\share\\foo": ["", "\\\\host\\share\\foo"]
+		"c:/foo/bar": ["c:/foo/", "bar"]
+		"c:\\foo\\bar": ["", "c:\\foo\\bar"]
+	}
+	windows: {
+		"/foo/bar/baz": ["/foo/bar/", "baz"]
+		"a/b": ["a/", "b"]
+		"//host/share/foo": ["//host/share/", "foo"]
+		"\\\\host\\share\\foo": ["\\\\host\\share\\", "foo"]
+		"c:/foo/bar": ["c:/foo/", "bar"]
+		"c:\\foo\\bar": ["c:\\foo\\", "bar"]
+	}
+}
+SplitList: {
+	unix: {
+		"a:b": ["a", "b"]
+		"a\u0000b": ["a\u0000b"]
+		"a;b": ["a;b"]
+	}
+	plan9: {
+		"a:b": ["a:b"]
+		"a\u0000b": ["a", "b"]
+		"a;b": ["a;b"]
+	}
+	windows: {
+		"a:b": ["a:b"]
+		"a\u0000b": ["a\u0000b"]
+		"a;b": ["a", "b"]
+	}
+}
+Clean: {
+	unix: {
+		"abc//def//ghi":        "abc/def/ghi"
+		"c:\\abc\\def\\..\\..": "c:\\abc\\def\\..\\.."
+	}
+	plan9: {
+		"abc//def//ghi":        "abc/def/ghi"
+		"c:\\abc\\def\\..\\..": "c:\\abc\\def\\..\\.."
+	}
+	default: {
+		"abc//def//ghi":        "abc/def/ghi"
+		"c:\\abc\\def\\..\\..": "c:\\abc\\def\\..\\.."
+	}
+	windows: {
+		"abc//def//ghi":        "abc\\def\\ghi"
+		"c:\\abc\\def\\..\\..": "c:\\"
+	}
+}
+Slash: {
+	unix: {
+		"": {
+			// should roundtrip
+			to:   ""
+			from: ""
+		}
+		"/": {
+			// should roundtrip
+			to:   "/"
+			from: "/"
+		}
+		"/a/b": {
+			// should roundtrip
+			to:   "/a/b"
+			from: "/a/b"
+		}
+		"/a//b": {
+			// should roundtrip
+			to:   "/a//b"
+			from: "/a//b"
+		}
+	}
+	plan9: {
+		"": {
+			// should roundtrip
+			to:   ""
+			from: ""
+		}
+		"/": {
+			// should roundtrip
+			to:   "/"
+			from: "/"
+		}
+		"/a/b": {
+			// should roundtrip
+			to:   "/a/b"
+			from: "/a/b"
+		}
+		"/a//b": {
+			// should roundtrip
+			to:   "/a//b"
+			from: "/a//b"
+		}
+	}
+	windows: {
+		"": {
+			// should roundtrip
+			to:   ""
+			from: ""
+		}
+		"/": {
+			// should roundtrip
+			to:   "/"
+			from: "\\"
+		}
+		"/a/b": {
+			// should roundtrip
+			to:   "/a/b"
+			from: "\\a\\b"
+		}
+		"/a//b": {
+			// should roundtrip
+			to:   "/a//b"
+			from: "\\a\\\\b"
+		}
+	}
+}
+Ext: {
+	unix: {
+		// Same for all OS-es
+		"path.go":    ".go"
+		"path.pb.go": ".go"
+		"a.dir/b":    ""
+		"a.dir/b.go": ".go"
+		"a.dir/":     ""
+
+		// Differs on Windows.
+		"a.dir\\foo": ".dir\\foo"
+	}
+	plan9: {
+		// Same for all OS-es
+		"path.go":    ".go"
+		"path.pb.go": ".go"
+		"a.dir/b":    ""
+		"a.dir/b.go": ".go"
+		"a.dir/":     ""
+
+		// Differs on Windows.
+		"a.dir\\foo": ".dir\\foo"
+	}
+	default: {
+		// Same for all OS-es
+		"path.go":    ".go"
+		"path.pb.go": ".go"
+		"a.dir/b":    ""
+		"a.dir/b.go": ".go"
+		"a.dir/":     ""
+
+		// Differs on Windows.
+		"a.dir\\foo": ".dir\\foo"
+	}
+	windows: {
+		// Same for all OS-es
+		"path.go":    ".go"
+		"path.pb.go": ".go"
+		"a.dir/b":    ""
+		"a.dir/b.go": ".go"
+		"a.dir/":     ""
+
+		// Differs on Windows.
+		"a.dir\\foo": ""
+	}
+}
+Resolve: {
+	unix: {
+		"a/b/c": {
+			"d/e": "a/b/c/d/e"
+		}
+		"/a/b": {
+			"/c/d": "/c/d"
+		}
+		"c:/a": {
+			"d:\\": "c:/a/d:\\"
+		}
+		"//home/user/foo": {
+			bar:               "/home/user/foo/bar"
+			"//other/abs/foo": "/other/abs/foo"
+		}
+	}
+	plan9: {
+		"a/b/c": {
+			"d/e": "a/b/c/d/e"
+		}
+		"/a/b": {
+			"/c/d": "/c/d"
+		}
+		"c:/a": {
+			"d:\\": "c:/a/d:\\"
+		}
+		"//home/user/foo": {
+			bar:               "/home/user/foo/bar"
+			"//other/abs/foo": "/other/abs/foo"
+		}
+	}
+	default: {
+		"a/b/c": {
+			"d/e": "a/b/c/d/e"
+		}
+		"/a/b": {
+			"/c/d": "/c/d"
+		}
+		"c:/a": {
+			"d:\\": "c:/a/d:\\"
+		}
+		"//home/user/foo": {
+			bar:               "/home/user/foo/bar"
+			"//other/abs/foo": "/other/abs/foo"
+		}
+	}
+	windows: {
+		"a/b/c": {
+			"d/e": "a\\b\\c\\d\\e"
+		}
+		"/a/b": {
+			"/c/d": "\\a\\b\\c\\d"
+		}
+		"c:/a": {
+			"d:\\": "d:\\"
+		}
+		"//home/user/foo": {
+			bar:               "\\\\home\\user\\foo\\bar"
+			"//other/abs/foo": "\\\\other\\abs\\foo"
+		}
+	}
+}
+IsAbs: {
+	unix: {
+		"":                false
+		"/a":              true
+		a:                 false
+		"c:":              false
+		"c:/":             false
+		"c:\\":            false
+		"//home/user/foo": true
+	}
+	plan9: {
+		"":                false
+		"/a":              true
+		a:                 false
+		"c:":              false
+		"c:/":             false
+		"c:\\":            false
+		"//home/user/foo": true
+	}
+	default: {
+		"":                false
+		"/a":              true
+		a:                 false
+		"c:":              false
+		"c:/":             false
+		"c:\\":            false
+		"//home/user/foo": true
+	}
+	windows: {
+		"":                false
+		"/a":              false
+		a:                 false
+		"c:":              false
+		"c:/":             true
+		"c:\\":            true
+		"//home/user/foo": true
+	}
+}
+Volume: {
+	unix: {
+		"c:/foo/bar":                                 ""
+		"c:":                                         ""
+		"2:":                                         ""
+		"":                                           ""
+		"\\\\\\host":                                 ""
+		"\\\\\\host\\":                               ""
+		"\\\\\\host\\share":                          ""
+		"\\\\\\host\\\\share":                        ""
+		"\\\\host":                                   ""
+		"//host":                                     ""
+		"\\\\host\\":                                 ""
+		"//host/":                                    ""
+		"\\\\host\\share":                            ""
+		"//host/share":                               ""
+		"\\\\host\\share\\":                          ""
+		"//host/share/":                              ""
+		"\\\\host\\share\\foo":                       ""
+		"//host/share/foo":                           ""
+		"\\\\host\\share\\\\foo\\\\\\bar\\\\\\\\baz": ""
+		"//host/share//foo///bar////baz":             ""
+		"\\\\host\\share\\foo\\..\\bar":              ""
+		"//host/share/foo/../bar":                    ""
+	}
+	plan9: {
+		"c:/foo/bar":                                 ""
+		"c:":                                         ""
+		"2:":                                         ""
+		"":                                           ""
+		"\\\\\\host":                                 ""
+		"\\\\\\host\\":                               ""
+		"\\\\\\host\\share":                          ""
+		"\\\\\\host\\\\share":                        ""
+		"\\\\host":                                   ""
+		"//host":                                     ""
+		"\\\\host\\":                                 ""
+		"//host/":                                    ""
+		"\\\\host\\share":                            ""
+		"//host/share":                               ""
+		"\\\\host\\share\\":                          ""
+		"//host/share/":                              ""
+		"\\\\host\\share\\foo":                       ""
+		"//host/share/foo":                           ""
+		"\\\\host\\share\\\\foo\\\\\\bar\\\\\\\\baz": ""
+		"//host/share//foo///bar////baz":             ""
+		"\\\\host\\share\\foo\\..\\bar":              ""
+		"//host/share/foo/../bar":                    ""
+	}
+	windows: {
+		"c:/foo/bar":                                 "c:"
+		"c:":                                         "c:"
+		"2:":                                         ""
+		"":                                           ""
+		"\\\\\\host":                                 ""
+		"\\\\\\host\\":                               ""
+		"\\\\\\host\\share":                          ""
+		"\\\\\\host\\\\share":                        ""
+		"\\\\host":                                   ""
+		"//host":                                     ""
+		"\\\\host\\":                                 ""
+		"//host/":                                    ""
+		"\\\\host\\share":                            "\\\\host\\share"
+		"//host/share":                               "//host/share"
+		"\\\\host\\share\\":                          "\\\\host\\share"
+		"//host/share/":                              "//host/share"
+		"\\\\host\\share\\foo":                       "\\\\host\\share"
+		"//host/share/foo":                           "//host/share"
+		"\\\\host\\share\\\\foo\\\\\\bar\\\\\\\\baz": "\\\\host\\share"
+		"//host/share//foo///bar////baz":             "//host/share"
+		"\\\\host\\share\\foo\\..\\bar":              "\\\\host\\share"
+		"//host/share/foo/../bar":                    "//host/share"
+	}
+}
+
diff --git a/pkg/path/testdata/path.go b/pkg/path/testdata/path.go
deleted file mode 100644
index b18883d..0000000
--- a/pkg/path/testdata/path.go
+++ /dev/null
@@ -1,414 +0,0 @@
-// Copyright 2020 CUE Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Copyright 2009 The Go 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 filepath implements utility routines for manipulating filename paths
-// in a way compatible with the target operating system-defined file paths.
-//
-// The filepath package uses either forward slashes or backslashes,
-// depending on the operating system. To process paths such as URLs
-// that always use forward slashes regardless of the operating
-// system, see the path package.
-package path
-
-import (
-	"errors"
-	"strings"
-)
-
-// A lazybuf is a lazily constructed path buffer.
-// It supports append, reading previously appended bytes,
-// and retrieving the final string. It does not allocate a buffer
-// to hold the output until that output diverges from s.
-type lazybuf struct {
-	path       string
-	buf        []byte
-	w          int
-	volAndPath string
-	volLen     int
-}
-
-func (b *lazybuf) index(i int) byte {
-	if b.buf != nil {
-		return b.buf[i]
-	}
-	return b.path[i]
-}
-
-func (b *lazybuf) append(c byte) {
-	if b.buf == nil {
-		if b.w < len(b.path) && b.path[b.w] == c {
-			b.w++
-			return
-		}
-		b.buf = make([]byte, len(b.path))
-		copy(b.buf, b.path[:b.w])
-	}
-	b.buf[b.w] = c
-	b.w++
-}
-
-func (b *lazybuf) string() string {
-	if b.buf == nil {
-		return b.volAndPath[:b.volLen+b.w]
-	}
-	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
-}
-
-// const (
-// 	Separator     = os.PathSeparator
-// 	ListSeparator = os.PathListSeparator
-// )
-
-// Clean returns the shortest path name equivalent to path
-// by purely lexical processing. The default value for os is Unix.
-// It applies the following rules
-// iteratively until no further processing can be done:
-//
-//	1. Replace multiple Separator elements with a single one.
-//	2. Eliminate each . path name element (the current directory).
-//	3. Eliminate each inner .. path name element (the parent directory)
-//	   along with the non-.. element that precedes it.
-//	4. Eliminate .. elements that begin a rooted path:
-//	   that is, replace "/.." by "/" at the beginning of a path,
-//	   assuming Separator is '/'.
-//
-// The returned path ends in a slash only if it represents a root directory,
-// such as "/" on Unix or `C:\` on Windows.
-//
-// Finally, any occurrences of slash are replaced by Separator.
-//
-// If the result of this process is an empty string, Clean
-// returns the string ".".
-//
-// See also Rob Pike, ``Lexical File Names in Plan 9 or
-// Getting Dot-Dot Right,''
-// https://9p.io/sys/doc/lexnames.html
-func Clean(path string, os OS) string {
-	return clean(path, getOS(os))
-}
-
-func clean(path string, os os) string {
-	originalPath := path
-	volLen := os.volumeNameLen(path)
-	path = path[volLen:]
-	if path == "" {
-		if volLen > 1 && originalPath[1] != ':' {
-			// should be UNC
-			return fromSlash(originalPath, os)
-		}
-		return originalPath + "."
-	}
-	rooted := os.IsPathSeparator(path[0])
-
-	// Invariants:
-	//	reading from path; r is index of next byte to process.
-	//	writing to buf; w is index of next byte to write.
-	//	dotdot is index in buf where .. must stop, either because
-	//		it is the leading slash or it is a leading ../../.. prefix.
-	n := len(path)
-	out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
-	r, dotdot := 0, 0
-	if rooted {
-		out.append(os.Separator)
-		r, dotdot = 1, 1
-	}
-
-	for r < n {
-		switch {
-		case os.IsPathSeparator(path[r]):
-			// empty path element
-			r++
-		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
-			// . element
-			r++
-		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
-			// .. element: remove to last separator
-			r += 2
-			switch {
-			case out.w > dotdot:
-				// can backtrack
-				out.w--
-				for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) {
-					out.w--
-				}
-			case !rooted:
-				// cannot backtrack, but not rooted, so append .. element.
-				if out.w > 0 {
-					out.append(os.Separator)
-				}
-				out.append('.')
-				out.append('.')
-				dotdot = out.w
-			}
-		default:
-			// real path element.
-			// add slash if needed
-			if rooted && out.w != 1 || !rooted && out.w != 0 {
-				out.append(os.Separator)
-			}
-			// copy element
-			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
-				out.append(path[r])
-			}
-		}
-	}
-
-	// Turn empty string into "."
-	if out.w == 0 {
-		out.append('.')
-	}
-
-	return fromSlash(out.string(), os)
-}
-
-// ToSlash returns the result of replacing each separator character
-// in path with a slash ('/') character. Multiple separators are
-// replaced by multiple slashes.
-func ToSlash(path string, os OS) string {
-	return toSlash(path, getOS(os))
-}
-
-func toSlash(path string, os os) string {
-	if os.Separator == '/' {
-		return path
-	}
-	return strings.ReplaceAll(path, string(os.Separator), "/")
-}
-
-// FromSlash returns the result of replacing each slash ('/') character
-// in path with a separator character. Multiple slashes are replaced
-// by multiple separators.
-func FromSlash(path string, os OS) string {
-	return fromSlash(path, getOS(os))
-}
-
-func fromSlash(path string, os os) string {
-	if os.Separator == '/' {
-		return path
-	}
-	return strings.ReplaceAll(path, "/", string(os.Separator))
-}
-
-// SplitList splits a list of paths joined by the OS-specific ListSeparator,
-// usually found in PATH or GOPATH environment variables.
-// Unlike strings.Split, SplitList returns an empty slice when passed an empty
-// string.
-func SplitList(path string, os OS) []string {
-	return getOS(os).splitList(path)
-}
-
-// Split splits path immediately following the final slash and returns them as
-// the list [dir, file], separating it into a directory and file name component.
-// If there is no slash in path, Split returns an empty dir and file set to
-// path. The returned values have the property that path = dir+file.
-// The default value for os is Unix.
-func Split(path string, os OS) []string {
-	x := getOS(os)
-	vol := volumeName(path, x)
-	i := len(path) - 1
-	for i >= len(vol) && !x.IsPathSeparator(path[i]) {
-		i--
-	}
-	return []string{path[:i+1], path[i+1:]}
-}
-
-// Join joins any number of path elements into a single path,
-// separating them with an OS specific Separator. Empty elements
-// are ignored. The result is Cleaned. However, if the argument
-// list is empty or all its elements are empty, Join returns
-// an empty string.
-// On Windows, the result will only be a UNC path if the first
-// non-empty element is a UNC path.
-// The default value for os is Unix.
-func Join(elem []string, os OS) string {
-	return getOS(os).join(elem)
-}
-
-// Ext returns the file name extension used by path.
-// The extension is the suffix beginning at the final dot
-// in the final element of path; it is empty if there is
-// no dot. The default value for os is Unix.
-func Ext(path string, os OS) string {
-	x := getOS(os)
-	for i := len(path) - 1; i >= 0 && !x.IsPathSeparator(path[i]); i-- {
-		if path[i] == '.' {
-			return path[i:]
-		}
-	}
-	return ""
-}
-
-// Resolve reports the path of sub relative to dir. If sub is an absolute path,
-// or if dir is empty, it will return sub. If sub is empty, it will return dir.
-// Resolve calls Clean on the result. The default value for os is Unix.
-func Resolve(dir, sub string, os OS) string {
-	x := getOS(os)
-	if x.IsAbs(sub) {
-		return clean(sub, x)
-	}
-	dir = clean(dir, x)
-	return x.join([]string{dir, sub})
-}
-
-// Rel returns a relative path that is lexically equivalent to targpath when
-// joined to basepath with an intervening separator. That is,
-// Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself.
-// On success, the returned path will always be relative to basepath,
-// even if basepath and targpath share no elements.
-// An error is returned if targpath can't be made relative to basepath or if
-// knowing the current working directory would be necessary to compute it.
-// Rel calls Clean on the result. The default value for os is Unix.
-func Rel(basepath, targpath string, os OS) (string, error) {
-	x := getOS(os)
-	baseVol := volumeName(basepath, x)
-	targVol := volumeName(targpath, x)
-	base := clean(basepath, x)
-	targ := clean(targpath, x)
-	if x.sameWord(targ, base) {
-		return ".", nil
-	}
-	base = base[len(baseVol):]
-	targ = targ[len(targVol):]
-	if base == "." {
-		base = ""
-	}
-	// Can't use IsAbs - `\a` and `a` are both relative in Windows.
-	baseSlashed := len(base) > 0 && base[0] == x.Separator
-	targSlashed := len(targ) > 0 && targ[0] == x.Separator
-	if baseSlashed != targSlashed || !x.sameWord(baseVol, targVol) {
-		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
-	}
-	// Position base[b0:bi] and targ[t0:ti] at the first differing elements.
-	bl := len(base)
-	tl := len(targ)
-	var b0, bi, t0, ti int
-	for {
-		for bi < bl && base[bi] != x.Separator {
-			bi++
-		}
-		for ti < tl && targ[ti] != x.Separator {
-			ti++
-		}
-		if !x.sameWord(targ[t0:ti], base[b0:bi]) {
-			break
-		}
-		if bi < bl {
-			bi++
-		}
-		if ti < tl {
-			ti++
-		}
-		b0 = bi
-		t0 = ti
-	}
-	if base[b0:bi] == ".." {
-		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
-	}
-	if b0 != bl {
-		// Base elements left. Must go up before going down.
-		seps := strings.Count(base[b0:bl], string(x.Separator))
-		size := 2 + seps*3
-		if tl != t0 {
-			size += 1 + tl - t0
-		}
-		buf := make([]byte, size)
-		n := copy(buf, "..")
-		for i := 0; i < seps; i++ {
-			buf[n] = x.Separator
-			copy(buf[n+1:], "..")
-			n += 3
-		}
-		if t0 != tl {
-			buf[n] = x.Separator
-			copy(buf[n+1:], targ[t0:])
-		}
-		return string(buf), nil
-	}
-	return targ[t0:], nil
-}
-
-// Base returns the last element of path.
-// Trailing path separators are removed before extracting the last element.
-// If the path is empty, Base returns ".".
-// If the path consists entirely of separators, Base returns a single separator.
-// The default value for os is Unix.
-func Base(path string, os OS) string {
-	x := getOS(os)
-	if path == "" {
-		return "."
-	}
-	// Strip trailing slashes.
-	for len(path) > 0 && x.IsPathSeparator(path[len(path)-1]) {
-		path = path[0 : len(path)-1]
-	}
-	// Throw away volume name
-	path = path[x.volumeNameLen(path):]
-	// Find the last element
-	i := len(path) - 1
-	for i >= 0 && !x.IsPathSeparator(path[i]) {
-		i--
-	}
-	if i >= 0 {
-		path = path[i+1:]
-	}
-	// If empty now, it had only slashes.
-	if path == "" {
-		return string(x.Separator)
-	}
-	return path
-}
-
-// Dir returns all but the last element of path, typically the path's directory.
-// After dropping the final element, Dir calls Clean on the path and trailing
-// slashes are removed.
-// If the path is empty, Dir returns ".".
-// If the path consists entirely of separators, Dir returns a single separator.
-// The returned path does not end in a separator unless it is the root directory.
-// The default value for os is Unix.
-func Dir(path string, os OS) string {
-	x := getOS(os)
-	vol := volumeName(path, x)
-	i := len(path) - 1
-	for i >= len(vol) && !x.IsPathSeparator(path[i]) {
-		i--
-	}
-	dir := clean(path[len(vol):i+1], x)
-	if dir == "." && len(vol) > 2 {
-		// must be UNC
-		return vol
-	}
-	return vol + dir
-}
-
-// IsAbs reports whether the path is absolute. The default value for os is Unix.
-func IsAbs(path string, os OS) bool {
-	return getOS(os).IsAbs(path)
-}
-
-// VolumeName returns leading volume name.
-// Given "C:\foo\bar" it returns "C:" on Windows.
-// Given "\\host\share\foo" it returns "\\host\share".
-// On other platforms it returns "".
-// The default value for os is Windows.
-func VolumeName(path string, os OS) string {
-	return volumeName(path, getOS(os))
-}
-
-func volumeName(path string, os os) string {
-	return path[:os.volumeNameLen(path)]
-}