pkg/path/testdata: adapt Go implementation

remove build tags and allow support for multiple OSes

Notes:
- evaluation is still fully hermetic
- many functions may default to Unix by default
- VolumeName defaults to Windows
- Resolve was added instead of Abs. The latter requires
  a Getwd(), which wouldn't be hermetic. We could also
  overload Join, and change its semantics, but this seems
  a bit error-prone.
- In windows both \ and / are interpreted as slashes in
  some contexts. It is therefore important to distinguish
  between using Separator and IsSeparator.
- Note the super-cool tests. :)

Questions:
- Should VolumeName even take an OS argument?

Change-Id: I080bee599700a98e6019ca0666f72b4a59ba8da1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7845
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/pkg/path/testdata/example_nix_test.go b/pkg/path/testdata/example_nix_test.go
index c9d6944..3c19da9 100644
--- a/pkg/path/testdata/example_nix_test.go
+++ b/pkg/path/testdata/example_nix_test.go
@@ -1,18 +1,31 @@
+// 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 2013 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.
 
-// +build !windows,!plan9
-
-package filepath_test
+package path_test
 
 import (
 	"fmt"
-	"path/filepath"
+
+	"cuelang.org/go/pkg/path"
 )
 
 func ExampleSplitList() {
-	fmt.Println("On Unix:", filepath.SplitList("/a/b/c:/usr/bin"))
+	fmt.Println("On Unix:", path.SplitList("/a/b/c:/usr/bin", path.Unix))
 	// Output:
 	// On Unix: [/a/b/c /usr/bin]
 }
@@ -27,7 +40,7 @@
 
 	fmt.Println("On Unix:")
 	for _, p := range paths {
-		rel, err := filepath.Rel(base, p)
+		rel, err := path.Rel(base, p, path.Unix)
 		fmt.Printf("%q: %q %v\n", p, rel, err)
 	}
 
@@ -47,8 +60,8 @@
 	}
 	fmt.Println("On Unix:")
 	for _, p := range paths {
-		dir, file := filepath.Split(p)
-		fmt.Printf("input: %q\n\tdir: %q\n\tfile: %q\n", p, dir, file)
+		pair := path.Split(p, path.Unix)
+		fmt.Printf("input: %q\n\tdir: %q\n\tfile: %q\n", p, pair[0], pair[1])
 	}
 	// Output:
 	// On Unix:
@@ -68,12 +81,12 @@
 
 func ExampleJoin() {
 	fmt.Println("On Unix:")
-	fmt.Println(filepath.Join("a", "b", "c"))
-	fmt.Println(filepath.Join("a", "b/c"))
-	fmt.Println(filepath.Join("a/b", "c"))
-	fmt.Println(filepath.Join("a/b", "/c"))
+	fmt.Println(path.Join([]string{"a", "b", "c"}, path.Unix))
+	fmt.Println(path.Join([]string{"a", "b/c"}, path.Unix))
+	fmt.Println(path.Join([]string{"a/b", "c"}, path.Unix))
+	fmt.Println(path.Join([]string{"a/b", "/c"}, path.Unix))
 
-	fmt.Println(filepath.Join("a/b", "../../../xyz"))
+	fmt.Println(path.Join([]string{"a/b", "../../../xyz"}, path.Unix))
 
 	// Output:
 	// On Unix:
@@ -86,10 +99,10 @@
 
 func ExampleMatch() {
 	fmt.Println("On Unix:")
-	fmt.Println(filepath.Match("/home/catch/*", "/home/catch/foo"))
-	fmt.Println(filepath.Match("/home/catch/*", "/home/catch/foo/bar"))
-	fmt.Println(filepath.Match("/home/?opher", "/home/gopher"))
-	fmt.Println(filepath.Match("/home/\\*", "/home/*"))
+	fmt.Println(path.Match("/home/catch/*", "/home/catch/foo", path.Unix))
+	fmt.Println(path.Match("/home/catch/*", "/home/catch/foo/bar", path.Unix))
+	fmt.Println(path.Match("/home/?opher", "/home/gopher", path.Unix))
+	fmt.Println(path.Match("/home/\\*", "/home/*", path.Unix))
 
 	// Output:
 	// On Unix:
@@ -101,15 +114,15 @@
 
 func ExampleBase() {
 	fmt.Println("On Unix:")
-	fmt.Println(filepath.Base("/foo/bar/baz.js"))
-	fmt.Println(filepath.Base("/foo/bar/baz"))
-	fmt.Println(filepath.Base("/foo/bar/baz/"))
-	fmt.Println(filepath.Base("dev.txt"))
-	fmt.Println(filepath.Base("../todo.txt"))
-	fmt.Println(filepath.Base(".."))
-	fmt.Println(filepath.Base("."))
-	fmt.Println(filepath.Base("/"))
-	fmt.Println(filepath.Base(""))
+	fmt.Println(path.Base("/foo/bar/baz.js", path.Unix))
+	fmt.Println(path.Base("/foo/bar/baz", path.Unix))
+	fmt.Println(path.Base("/foo/bar/baz/", path.Unix))
+	fmt.Println(path.Base("dev.txt", path.Unix))
+	fmt.Println(path.Base("../todo.txt", path.Unix))
+	fmt.Println(path.Base("..", path.Unix))
+	fmt.Println(path.Base(".", path.Unix))
+	fmt.Println(path.Base("/", path.Unix))
+	fmt.Println(path.Base("", path.Unix))
 
 	// Output:
 	// On Unix:
@@ -126,16 +139,16 @@
 
 func ExampleDir() {
 	fmt.Println("On Unix:")
-	fmt.Println(filepath.Dir("/foo/bar/baz.js"))
-	fmt.Println(filepath.Dir("/foo/bar/baz"))
-	fmt.Println(filepath.Dir("/foo/bar/baz/"))
-	fmt.Println(filepath.Dir("/dirty//path///"))
-	fmt.Println(filepath.Dir("dev.txt"))
-	fmt.Println(filepath.Dir("../todo.txt"))
-	fmt.Println(filepath.Dir(".."))
-	fmt.Println(filepath.Dir("."))
-	fmt.Println(filepath.Dir("/"))
-	fmt.Println(filepath.Dir(""))
+	fmt.Println(path.Dir("/foo/bar/baz.js", path.Unix))
+	fmt.Println(path.Dir("/foo/bar/baz", path.Unix))
+	fmt.Println(path.Dir("/foo/bar/baz/", path.Unix))
+	fmt.Println(path.Dir("/dirty//path///", path.Unix))
+	fmt.Println(path.Dir("dev.txt", path.Unix))
+	fmt.Println(path.Dir("../todo.txt", path.Unix))
+	fmt.Println(path.Dir("..", path.Unix))
+	fmt.Println(path.Dir(".", path.Unix))
+	fmt.Println(path.Dir("/", path.Unix))
+	fmt.Println(path.Dir("", path.Unix))
 
 	// Output:
 	// On Unix:
@@ -153,12 +166,12 @@
 
 func ExampleIsAbs() {
 	fmt.Println("On Unix:")
-	fmt.Println(filepath.IsAbs("/home/gopher"))
-	fmt.Println(filepath.IsAbs(".bashrc"))
-	fmt.Println(filepath.IsAbs(".."))
-	fmt.Println(filepath.IsAbs("."))
-	fmt.Println(filepath.IsAbs("/"))
-	fmt.Println(filepath.IsAbs(""))
+	fmt.Println(path.IsAbs("/home/gopher", path.Unix))
+	fmt.Println(path.IsAbs(".bashrc", path.Unix))
+	fmt.Println(path.IsAbs("..", path.Unix))
+	fmt.Println(path.IsAbs(".", path.Unix))
+	fmt.Println(path.IsAbs("/", path.Unix))
+	fmt.Println(path.IsAbs("", path.Unix))
 
 	// Output:
 	// On Unix:
diff --git a/pkg/path/testdata/example_test.go b/pkg/path/testdata/example_test.go
index a1d680e..726de43 100644
--- a/pkg/path/testdata/example_test.go
+++ b/pkg/path/testdata/example_test.go
@@ -1,18 +1,33 @@
+// 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 2017 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_test
+package path_test
 
 import (
 	"fmt"
-	"path/filepath"
+
+	"cuelang.org/go/pkg/path"
 )
 
 func ExampleExt() {
-	fmt.Printf("No dots: %q\n", filepath.Ext("index"))
-	fmt.Printf("One dot: %q\n", filepath.Ext("index.js"))
-	fmt.Printf("Two dots: %q\n", filepath.Ext("main.test.js"))
+	fmt.Printf("No dots: %q\n", path.Ext("index", "unix"))
+	fmt.Printf("One dot: %q\n", path.Ext("index.js", "unix"))
+	fmt.Printf("Two dots: %q\n", path.Ext("main.test.js", "unix"))
 	// Output:
 	// No dots: ""
 	// One dot: ".js"
diff --git a/pkg/path/testdata/example_unix_walk_test.go b/pkg/path/testdata/example_unix_walk_test.go
deleted file mode 100644
index 66dc7f6..0000000
--- a/pkg/path/testdata/example_unix_walk_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2018 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.
-
-// +build !windows,!plan9
-
-package filepath_test
-
-import (
-	"fmt"
-	"io/fs"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-)
-
-func prepareTestDirTree(tree string) (string, error) {
-	tmpDir, err := ioutil.TempDir("", "")
-	if err != nil {
-		return "", fmt.Errorf("error creating temp directory: %v\n", err)
-	}
-
-	err = os.MkdirAll(filepath.Join(tmpDir, tree), 0755)
-	if err != nil {
-		os.RemoveAll(tmpDir)
-		return "", err
-	}
-
-	return tmpDir, nil
-}
-
-func ExampleWalk() {
-	tmpDir, err := prepareTestDirTree("dir/to/walk/skip")
-	if err != nil {
-		fmt.Printf("unable to create test dir tree: %v\n", err)
-		return
-	}
-	defer os.RemoveAll(tmpDir)
-	os.Chdir(tmpDir)
-
-	subDirToSkip := "skip"
-
-	fmt.Println("On Unix:")
-	err = filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
-		if err != nil {
-			fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err)
-			return err
-		}
-		if info.IsDir() && info.Name() == subDirToSkip {
-			fmt.Printf("skipping a dir without errors: %+v \n", info.Name())
-			return filepath.SkipDir
-		}
-		fmt.Printf("visited file or dir: %q\n", path)
-		return nil
-	})
-	if err != nil {
-		fmt.Printf("error walking the path %q: %v\n", tmpDir, err)
-		return
-	}
-	// Output:
-	// On Unix:
-	// visited file or dir: "."
-	// visited file or dir: "dir"
-	// visited file or dir: "dir/to"
-	// visited file or dir: "dir/to/walk"
-	// skipping a dir without errors: skip
-}
diff --git a/pkg/path/testdata/export_test.go b/pkg/path/testdata/export_test.go
deleted file mode 100644
index 0cf9e3b..0000000
--- a/pkg/path/testdata/export_test.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright 2013 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
-
-var LstatP = &lstat
diff --git a/pkg/path/testdata/export_windows_test.go b/pkg/path/testdata/export_windows_test.go
deleted file mode 100644
index a7e2e64..0000000
--- a/pkg/path/testdata/export_windows_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2016 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
-
-var (
-	ToNorm   = toNorm
-	NormBase = normBase
-)
diff --git a/pkg/path/testdata/match.go b/pkg/path/testdata/match.go
index c77a269..b18d78c 100644
--- a/pkg/path/testdata/match.go
+++ b/pkg/path/testdata/match.go
@@ -1,14 +1,25 @@
+// 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 2010 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
+package path
 
 import (
 	"errors"
-	"os"
-	"runtime"
-	"sort"
 	"strings"
 	"unicode/utf8"
 )
@@ -41,18 +52,19 @@
 // On Windows, escaping is disabled. Instead, '\\' is treated as
 // path separator.
 //
-func Match(pattern, name string) (matched bool, err error) {
+func Match(pattern, name string, o OS) (matched bool, err error) {
+	os := getOS(o)
 Pattern:
 	for len(pattern) > 0 {
 		var star bool
 		var chunk string
-		star, chunk, pattern = scanChunk(pattern)
+		star, chunk, pattern = scanChunk(pattern, os)
 		if star && chunk == "" {
 			// Trailing * matches rest of string unless it has a /.
-			return !strings.Contains(name, string(Separator)), nil
+			return !strings.Contains(name, string(os.Separator)), nil
 		}
 		// Look for match at current position.
-		t, ok, err := matchChunk(chunk, name)
+		t, ok, err := matchChunk(chunk, name, os)
 		// if we're the last chunk, make sure we've exhausted the name
 		// otherwise we'll give a false result even if we could still match
 		// using the star
@@ -66,8 +78,8 @@
 		if star {
 			// Look for match skipping i+1 bytes.
 			// Cannot skip /.
-			for i := 0; i < len(name) && name[i] != Separator; i++ {
-				t, ok, err := matchChunk(chunk, name[i+1:])
+			for i := 0; i < len(name) && name[i] != os.Separator; i++ {
+				t, ok, err := matchChunk(chunk, name[i+1:], os)
 				if ok {
 					// if we're the last chunk, make sure we exhausted the name
 					if len(pattern) == 0 && len(t) > 0 {
@@ -88,7 +100,7 @@
 
 // scanChunk gets the next segment of pattern, which is a non-star string
 // possibly preceded by a star.
-func scanChunk(pattern string) (star bool, chunk, rest string) {
+func scanChunk(pattern string, os os) (star bool, chunk, rest string) {
 	for len(pattern) > 0 && pattern[0] == '*' {
 		pattern = pattern[1:]
 		star = true
@@ -99,7 +111,7 @@
 	for i = 0; i < len(pattern); i++ {
 		switch pattern[i] {
 		case '\\':
-			if runtime.GOOS != "windows" {
+			if !os.isWindows() {
 				// error check handled in matchChunk: bad pattern.
 				if i+1 < len(pattern) {
 					i++
@@ -121,7 +133,7 @@
 // matchChunk checks whether chunk matches the beginning of s.
 // If so, it returns the remainder of s (after the match).
 // Chunk is all single-character operators: literals, char classes, and ?.
-func matchChunk(chunk, s string) (rest string, ok bool, err error) {
+func matchChunk(chunk, s string, os os) (rest string, ok bool, err error) {
 	// failed records whether the match has failed.
 	// After the match fails, the loop continues on processing chunk,
 	// checking that the pattern is well-formed but no longer reading s.
@@ -155,12 +167,12 @@
 					break
 				}
 				var lo, hi rune
-				if lo, chunk, err = getEsc(chunk); err != nil {
+				if lo, chunk, err = getEsc(chunk, os); err != nil {
 					return "", false, err
 				}
 				hi = lo
 				if chunk[0] == '-' {
-					if hi, chunk, err = getEsc(chunk[1:]); err != nil {
+					if hi, chunk, err = getEsc(chunk[1:], os); err != nil {
 						return "", false, err
 					}
 				}
@@ -175,7 +187,7 @@
 
 		case '?':
 			if !failed {
-				if s[0] == Separator {
+				if s[0] == os.Separator {
 					failed = true
 				}
 				_, n := utf8.DecodeRuneInString(s)
@@ -184,7 +196,7 @@
 			chunk = chunk[1:]
 
 		case '\\':
-			if runtime.GOOS != "windows" {
+			if !os.isWindows() {
 				chunk = chunk[1:]
 				if len(chunk) == 0 {
 					return "", false, ErrBadPattern
@@ -209,12 +221,12 @@
 }
 
 // getEsc gets a possibly-escaped character from chunk, for a character class.
-func getEsc(chunk string) (r rune, nchunk string, err error) {
+func getEsc(chunk string, os os) (r rune, nchunk string, err error) {
 	if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
 		err = ErrBadPattern
 		return
 	}
-	if chunk[0] == '\\' && runtime.GOOS != "windows" {
+	if chunk[0] == '\\' && !os.isWindows() {
 		chunk = chunk[1:]
 		if len(chunk) == 0 {
 			err = ErrBadPattern
@@ -231,130 +243,3 @@
 	}
 	return
 }
-
-// Glob returns the names of all files matching pattern or nil
-// if there is no matching file. The syntax of patterns is the same
-// as in Match. The pattern may describe hierarchical names such as
-// /usr/*/bin/ed (assuming the Separator is '/').
-//
-// Glob ignores file system errors such as I/O errors reading directories.
-// The only possible returned error is ErrBadPattern, when pattern
-// is malformed.
-func Glob(pattern string) (matches []string, err error) {
-	// Check pattern is well-formed.
-	if _, err := Match(pattern, ""); err != nil {
-		return nil, err
-	}
-	if !hasMeta(pattern) {
-		if _, err = os.Lstat(pattern); err != nil {
-			return nil, nil
-		}
-		return []string{pattern}, nil
-	}
-
-	dir, file := Split(pattern)
-	volumeLen := 0
-	if runtime.GOOS == "windows" {
-		volumeLen, dir = cleanGlobPathWindows(dir)
-	} else {
-		dir = cleanGlobPath(dir)
-	}
-
-	if !hasMeta(dir[volumeLen:]) {
-		return glob(dir, file, nil)
-	}
-
-	// Prevent infinite recursion. See issue 15879.
-	if dir == pattern {
-		return nil, ErrBadPattern
-	}
-
-	var m []string
-	m, err = Glob(dir)
-	if err != nil {
-		return
-	}
-	for _, d := range m {
-		matches, err = glob(d, file, matches)
-		if err != nil {
-			return
-		}
-	}
-	return
-}
-
-// cleanGlobPath prepares path for glob matching.
-func cleanGlobPath(path string) string {
-	switch path {
-	case "":
-		return "."
-	case string(Separator):
-		// do nothing to the path
-		return path
-	default:
-		return path[0 : len(path)-1] // chop off trailing separator
-	}
-}
-
-// cleanGlobPathWindows is windows version of cleanGlobPath.
-func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) {
-	vollen := volumeNameLen(path)
-	switch {
-	case path == "":
-		return 0, "."
-	case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /, \, C:\ and C:/
-		// do nothing to the path
-		return vollen + 1, path
-	case vollen == len(path) && len(path) == 2: // C:
-		return vollen, path + "." // convert C: into C:.
-	default:
-		if vollen >= len(path) {
-			vollen = len(path) - 1
-		}
-		return vollen, path[0 : len(path)-1] // chop off trailing separator
-	}
-}
-
-// glob searches for files matching pattern in the directory dir
-// and appends them to matches. If the directory cannot be
-// opened, it returns the existing matches. New matches are
-// added in lexicographical order.
-func glob(dir, pattern string, matches []string) (m []string, e error) {
-	m = matches
-	fi, err := os.Stat(dir)
-	if err != nil {
-		return // ignore I/O error
-	}
-	if !fi.IsDir() {
-		return // ignore I/O error
-	}
-	d, err := os.Open(dir)
-	if err != nil {
-		return // ignore I/O error
-	}
-	defer d.Close()
-
-	names, _ := d.Readdirnames(-1)
-	sort.Strings(names)
-
-	for _, n := range names {
-		matched, err := Match(pattern, n)
-		if err != nil {
-			return m, err
-		}
-		if matched {
-			m = append(m, Join(dir, n))
-		}
-	}
-	return
-}
-
-// hasMeta reports whether path contains any of the magic characters
-// recognized by Match.
-func hasMeta(path string) bool {
-	magicChars := `*?[`
-	if runtime.GOOS != "windows" {
-		magicChars = `*?[\`
-	}
-	return strings.ContainsAny(path, magicChars)
-}
diff --git a/pkg/path/testdata/match_test.go b/pkg/path/testdata/match_test.go
index 1c3b567..2b82e10 100644
--- a/pkg/path/testdata/match_test.go
+++ b/pkg/path/testdata/match_test.go
@@ -1,18 +1,24 @@
+// 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_test
+package path
 
 import (
-	"fmt"
-	"internal/testenv"
-	"io/ioutil"
-	"os"
-	. "path/filepath"
-	"reflect"
-	"runtime"
-	"sort"
 	"strings"
 	"testing"
 )
@@ -90,304 +96,23 @@
 }
 
 func TestMatch(t *testing.T) {
-	for _, tt := range matchTests {
-		pattern := tt.pattern
-		s := tt.s
-		if runtime.GOOS == "windows" {
-			if strings.Contains(pattern, "\\") {
-				// no escape allowed on windows.
-				continue
+	for _, os := range []OS{Unix, Windows, Plan9} {
+		for _, tt := range matchTests {
+			pattern := tt.pattern
+			s := tt.s
+			if os == Windows {
+				if strings.Contains(pattern, "\\") {
+					// no escape allowed on windows.
+					continue
+				}
+				pattern = Clean(pattern, os)
+				s = Clean(s, os)
 			}
-			pattern = Clean(pattern)
-			s = Clean(s)
+			ok, err := Match(pattern, s, os)
+			if ok != tt.match || err != tt.err {
+				t.Errorf("Match(%#q, %#q, %q) = %v, %q want %v, %q",
+					pattern, s, os, ok, errp(err), tt.match, errp(tt.err))
+			}
 		}
-		ok, err := Match(pattern, s)
-		if ok != tt.match || err != tt.err {
-			t.Errorf("Match(%#q, %#q) = %v, %q want %v, %q", pattern, s, ok, errp(err), tt.match, errp(tt.err))
-		}
-	}
-}
-
-// contains reports whether vector contains the string s.
-func contains(vector []string, s string) bool {
-	for _, elem := range vector {
-		if elem == s {
-			return true
-		}
-	}
-	return false
-}
-
-var globTests = []struct {
-	pattern, result string
-}{
-	{"match.go", "match.go"},
-	{"mat?h.go", "match.go"},
-	{"*", "match.go"},
-	{"../*/match.go", "../filepath/match.go"},
-}
-
-func TestGlob(t *testing.T) {
-	for _, tt := range globTests {
-		pattern := tt.pattern
-		result := tt.result
-		if runtime.GOOS == "windows" {
-			pattern = Clean(pattern)
-			result = Clean(result)
-		}
-		matches, err := Glob(pattern)
-		if err != nil {
-			t.Errorf("Glob error for %q: %s", pattern, err)
-			continue
-		}
-		if !contains(matches, result) {
-			t.Errorf("Glob(%#q) = %#v want %v", pattern, matches, result)
-		}
-	}
-	for _, pattern := range []string{"no_match", "../*/no_match"} {
-		matches, err := Glob(pattern)
-		if err != nil {
-			t.Errorf("Glob error for %q: %s", pattern, err)
-			continue
-		}
-		if len(matches) != 0 {
-			t.Errorf("Glob(%#q) = %#v want []", pattern, matches)
-		}
-	}
-}
-
-func TestGlobError(t *testing.T) {
-	bad := []string{`[]`, `nonexist/[]`}
-	for _, pattern := range bad {
-		if _, err := Glob(pattern); err != ErrBadPattern {
-			t.Errorf("Glob(%#q) returned err=%v, want ErrBadPattern", pattern, err)
-		}
-	}
-}
-
-func TestGlobUNC(t *testing.T) {
-	// Just make sure this runs without crashing for now.
-	// See issue 15879.
-	Glob(`\\?\C:\*`)
-}
-
-var globSymlinkTests = []struct {
-	path, dest string
-	brokenLink bool
-}{
-	{"test1", "link1", false},
-	{"test2", "link2", true},
-}
-
-func TestGlobSymlink(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	tmpDir, err := ioutil.TempDir("", "globsymlink")
-	if err != nil {
-		t.Fatal("creating temp dir:", err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	for _, tt := range globSymlinkTests {
-		path := Join(tmpDir, tt.path)
-		dest := Join(tmpDir, tt.dest)
-		f, err := os.Create(path)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if err := f.Close(); err != nil {
-			t.Fatal(err)
-		}
-		err = os.Symlink(path, dest)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if tt.brokenLink {
-			// Break the symlink.
-			os.Remove(path)
-		}
-		matches, err := Glob(dest)
-		if err != nil {
-			t.Errorf("GlobSymlink error for %q: %s", dest, err)
-		}
-		if !contains(matches, dest) {
-			t.Errorf("Glob(%#q) = %#v want %v", dest, matches, dest)
-		}
-	}
-}
-
-type globTest struct {
-	pattern string
-	matches []string
-}
-
-func (test *globTest) buildWant(root string) []string {
-	want := make([]string, 0)
-	for _, m := range test.matches {
-		want = append(want, root+FromSlash(m))
-	}
-	sort.Strings(want)
-	return want
-}
-
-func (test *globTest) globAbs(root, rootPattern string) error {
-	p := FromSlash(rootPattern + `\` + test.pattern)
-	have, err := Glob(p)
-	if err != nil {
-		return err
-	}
-	sort.Strings(have)
-	want := test.buildWant(root + `\`)
-	if strings.Join(want, "_") == strings.Join(have, "_") {
-		return nil
-	}
-	return fmt.Errorf("Glob(%q) returns %q, but %q expected", p, have, want)
-}
-
-func (test *globTest) globRel(root string) error {
-	p := root + FromSlash(test.pattern)
-	have, err := Glob(p)
-	if err != nil {
-		return err
-	}
-	sort.Strings(have)
-	want := test.buildWant(root)
-	if strings.Join(want, "_") == strings.Join(have, "_") {
-		return nil
-	}
-	// try also matching version without root prefix
-	wantWithNoRoot := test.buildWant("")
-	if strings.Join(wantWithNoRoot, "_") == strings.Join(have, "_") {
-		return nil
-	}
-	return fmt.Errorf("Glob(%q) returns %q, but %q expected", p, have, want)
-}
-
-func TestWindowsGlob(t *testing.T) {
-	if runtime.GOOS != "windows" {
-		t.Skipf("skipping windows specific test")
-	}
-
-	tmpDir, err := ioutil.TempDir("", "TestWindowsGlob")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	// /tmp may itself be a symlink
-	tmpDir, err = EvalSymlinks(tmpDir)
-	if err != nil {
-		t.Fatal("eval symlink for tmp dir:", err)
-	}
-
-	if len(tmpDir) < 3 {
-		t.Fatalf("tmpDir path %q is too short", tmpDir)
-	}
-	if tmpDir[1] != ':' {
-		t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir)
-	}
-
-	dirs := []string{
-		"a",
-		"b",
-		"dir/d/bin",
-	}
-	files := []string{
-		"dir/d/bin/git.exe",
-	}
-	for _, dir := range dirs {
-		err := os.MkdirAll(Join(tmpDir, dir), 0777)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}
-	for _, file := range files {
-		err := ioutil.WriteFile(Join(tmpDir, file), nil, 0666)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}
-
-	tests := []globTest{
-		{"a", []string{"a"}},
-		{"b", []string{"b"}},
-		{"c", []string{}},
-		{"*", []string{"a", "b", "dir"}},
-		{"d*", []string{"dir"}},
-		{"*i*", []string{"dir"}},
-		{"*r", []string{"dir"}},
-		{"?ir", []string{"dir"}},
-		{"?r", []string{}},
-		{"d*/*/bin/git.exe", []string{"dir/d/bin/git.exe"}},
-	}
-
-	// test absolute paths
-	for _, test := range tests {
-		var p string
-		err = test.globAbs(tmpDir, tmpDir)
-		if err != nil {
-			t.Error(err)
-		}
-		// test C:\*Documents and Settings\...
-		p = tmpDir
-		p = strings.Replace(p, `:\`, `:\*`, 1)
-		err = test.globAbs(tmpDir, p)
-		if err != nil {
-			t.Error(err)
-		}
-		// test C:\Documents and Settings*\...
-		p = tmpDir
-		p = strings.Replace(p, `:\`, `:`, 1)
-		p = strings.Replace(p, `\`, `*\`, 1)
-		p = strings.Replace(p, `:`, `:\`, 1)
-		err = test.globAbs(tmpDir, p)
-		if err != nil {
-			t.Error(err)
-		}
-	}
-
-	// test relative paths
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-	err = os.Chdir(tmpDir)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		err := os.Chdir(wd)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}()
-	for _, test := range tests {
-		err := test.globRel("")
-		if err != nil {
-			t.Error(err)
-		}
-		err = test.globRel(`.\`)
-		if err != nil {
-			t.Error(err)
-		}
-		err = test.globRel(tmpDir[:2]) // C:
-		if err != nil {
-			t.Error(err)
-		}
-	}
-}
-
-func TestNonWindowsGlobEscape(t *testing.T) {
-	if runtime.GOOS == "windows" {
-		t.Skipf("skipping non-windows specific test")
-	}
-	pattern := `\match.go`
-	want := []string{"match.go"}
-	matches, err := Glob(pattern)
-	if err != nil {
-		t.Fatalf("Glob error for %q: %s", pattern, err)
-	}
-	if !reflect.DeepEqual(matches, want) {
-		t.Fatalf("Glob(%#q) = %v want %v", pattern, matches, want)
 	}
 }
diff --git a/pkg/path/testdata/os.go b/pkg/path/testdata/os.go
new file mode 100644
index 0000000..d5de0be
--- /dev/null
+++ b/pkg/path/testdata/os.go
@@ -0,0 +1,75 @@
+// 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
+
+type OS string
+
+const (
+	Unix    OS = "unix"
+	Windows OS = "windows"
+	Plan9   OS = "plan9"
+)
+
+// These types have been designed to minimize the diffs with the original Go
+// code, thereby minimizing potential toil in keeping them up to date.
+
+type os struct {
+	osInfo
+	Separator     byte
+	ListSeparator byte
+}
+
+func (o os) isWindows() bool {
+	return o.Separator == '\\'
+}
+
+type osInfo interface {
+	IsPathSeparator(b byte) bool
+	splitList(path string) []string
+	volumeNameLen(path string) int
+	IsAbs(path string) (b bool)
+	HasPrefix(p, prefix string) bool
+	join(elem []string) string
+	sameWord(a, b string) bool
+}
+
+func getOS(o OS) os {
+	switch o {
+	case Windows:
+		return windows
+	case Plan9:
+		return plan9
+	default:
+		return unix
+	}
+}
+
+var (
+	plan9 = os{
+		osInfo:        &plan9Info{},
+		Separator:     plan9Separator,
+		ListSeparator: plan9ListSeparator,
+	}
+	unix = os{
+		osInfo:        &unixInfo{},
+		Separator:     unixSeparator,
+		ListSeparator: unixListSeparator,
+	}
+	windows = os{
+		osInfo:        &windowsInfo{},
+		Separator:     windowsSeparator,
+		ListSeparator: windowsListSeparator,
+	}
+)
diff --git a/pkg/path/testdata/path.go b/pkg/path/testdata/path.go
index 2e7b439..b18883d 100644
--- a/pkg/path/testdata/path.go
+++ b/pkg/path/testdata/path.go
@@ -1,3 +1,17 @@
+// 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.
@@ -9,13 +23,10 @@
 // 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 filepath
+package path
 
 import (
 	"errors"
-	"io/fs"
-	"os"
-	"sort"
 	"strings"
 )
 
@@ -58,13 +69,14 @@
 	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
 }
 
-const (
-	Separator     = os.PathSeparator
-	ListSeparator = os.PathListSeparator
-)
+// const (
+// 	Separator     = os.PathSeparator
+// 	ListSeparator = os.PathListSeparator
+// )
 
 // 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 Separator elements with a single one.
@@ -86,14 +98,18 @@
 // 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 {
+func Clean(path string, os OS) string {
+	return clean(path, getOS(os))
+}
+
+func clean(path string, os os) string {
 	originalPath := path
-	volLen := volumeNameLen(path)
+	volLen := os.volumeNameLen(path)
 	path = path[volLen:]
 	if path == "" {
 		if volLen > 1 && originalPath[1] != ':' {
 			// should be UNC
-			return FromSlash(originalPath)
+			return fromSlash(originalPath, os)
 		}
 		return originalPath + "."
 	}
@@ -108,7 +124,7 @@
 	out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
 	r, dotdot := 0, 0
 	if rooted {
-		out.append(Separator)
+		out.append(os.Separator)
 		r, dotdot = 1, 1
 	}
 
@@ -133,7 +149,7 @@
 			case !rooted:
 				// cannot backtrack, but not rooted, so append .. element.
 				if out.w > 0 {
-					out.append(Separator)
+					out.append(os.Separator)
 				}
 				out.append('.')
 				out.append('.')
@@ -143,7 +159,7 @@
 			// real path element.
 			// add slash if needed
 			if rooted && out.w != 1 || !rooted && out.w != 0 {
-				out.append(Separator)
+				out.append(os.Separator)
 			}
 			// copy element
 			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
@@ -157,49 +173,58 @@
 		out.append('.')
 	}
 
-	return FromSlash(out.string())
+	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) string {
-	if Separator == '/' {
+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(Separator), "/")
+	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) string {
-	if Separator == '/' {
+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(Separator))
+	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) []string {
-	return splitList(path)
+func SplitList(path string, os OS) []string {
+	return getOS(os).splitList(path)
 }
 
-// Split splits path immediately following the final Separator,
-// separating it into a directory and file name component.
-// If there is no Separator 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) (dir, file string) {
-	vol := VolumeName(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) && !os.IsPathSeparator(path[i]) {
+	for i >= len(vol) && !x.IsPathSeparator(path[i]) {
 		i--
 	}
-	return path[:i+1], path[i+1:]
+	return []string{path[:i+1], path[i+1:]}
 }
 
 // Join joins any number of path elements into a single path,
@@ -209,16 +234,18 @@
 // an empty string.
 // On Windows, the result will only be a UNC path if the first
 // non-empty element is a UNC path.
-func Join(elem ...string) string {
-	return join(elem)
+// 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.
-func Ext(path string) string {
-	for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- {
+// 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:]
 		}
@@ -226,33 +253,16 @@
 	return ""
 }
 
-// EvalSymlinks returns the path name after the evaluation of any symbolic
-// links.
-// If path is relative the result will be relative to the current directory,
-// unless one of the components is an absolute symbolic link.
-// EvalSymlinks calls Clean on the result.
-func EvalSymlinks(path string) (string, error) {
-	return evalSymlinks(path)
-}
-
-// Abs returns an absolute representation of path.
-// If the path is not absolute it will be joined with the current
-// working directory to turn it into an absolute path. The absolute
-// path name for a given file is not guaranteed to be unique.
-// Abs calls Clean on the result.
-func Abs(path string) (string, error) {
-	return abs(path)
-}
-
-func unixAbs(path string) (string, error) {
-	if IsAbs(path) {
-		return Clean(path), nil
+// 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)
 	}
-	wd, err := os.Getwd()
-	if err != nil {
-		return "", err
-	}
-	return Join(wd, path), nil
+	dir = clean(dir, x)
+	return x.join([]string{dir, sub})
 }
 
 // Rel returns a relative path that is lexically equivalent to targpath when
@@ -262,13 +272,14 @@
 // 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.
-func Rel(basepath, targpath string) (string, error) {
-	baseVol := VolumeName(basepath)
-	targVol := VolumeName(targpath)
-	base := Clean(basepath)
-	targ := Clean(targpath)
-	if sameWord(targ, base) {
+// 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):]
@@ -277,9 +288,9 @@
 		base = ""
 	}
 	// Can't use IsAbs - `\a` and `a` are both relative in Windows.
-	baseSlashed := len(base) > 0 && base[0] == Separator
-	targSlashed := len(targ) > 0 && targ[0] == Separator
-	if baseSlashed != targSlashed || !sameWord(baseVol, targVol) {
+	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.
@@ -287,13 +298,13 @@
 	tl := len(targ)
 	var b0, bi, t0, ti int
 	for {
-		for bi < bl && base[bi] != Separator {
+		for bi < bl && base[bi] != x.Separator {
 			bi++
 		}
-		for ti < tl && targ[ti] != Separator {
+		for ti < tl && targ[ti] != x.Separator {
 			ti++
 		}
-		if !sameWord(targ[t0:ti], base[b0:bi]) {
+		if !x.sameWord(targ[t0:ti], base[b0:bi]) {
 			break
 		}
 		if bi < bl {
@@ -310,7 +321,7 @@
 	}
 	if b0 != bl {
 		// Base elements left. Must go up before going down.
-		seps := strings.Count(base[b0:bl], string(Separator))
+		seps := strings.Count(base[b0:bl], string(x.Separator))
 		size := 2 + seps*3
 		if tl != t0 {
 			size += 1 + tl - t0
@@ -318,12 +329,12 @@
 		buf := make([]byte, size)
 		n := copy(buf, "..")
 		for i := 0; i < seps; i++ {
-			buf[n] = Separator
+			buf[n] = x.Separator
 			copy(buf[n+1:], "..")
 			n += 3
 		}
 		if t0 != tl {
-			buf[n] = Separator
+			buf[n] = x.Separator
 			copy(buf[n+1:], targ[t0:])
 		}
 		return string(buf), nil
@@ -331,230 +342,25 @@
 	return targ[t0:], nil
 }
 
-// SkipDir is used as a return value from WalkFuncs to indicate that
-// the directory named in the call is to be skipped. It is not returned
-// as an error by any function.
-var SkipDir error = fs.SkipDir
-
-// WalkFunc is the type of the function called by Walk to visit each each
-// file or directory.
-//
-// The path argument contains the argument to Walk as a prefix.
-// That is, if Walk is called with root argument "dir" and finds a file
-// named "a" in that directory, the walk function will be called with
-// argument "dir/a".
-//
-// The directory and file are joined with Join, which may clean the
-// directory name: if Walk is called with the root argument "x/../dir"
-// and finds a file named "a" in that directory, the walk function will
-// be called with argument "dir/a", not "x/../dir/a".
-//
-// The info argument is the fs.FileInfo for the named path.
-//
-// The error result returned by the function controls how Walk continues.
-// If the function returns the special value SkipDir, Walk skips the
-// current directory (path if info.IsDir() is true, otherwise path's
-// parent directory). Otherwise, if the function returns a non-nil error,
-// Walk stops entirely and returns that error.
-//
-// The err argument reports an error related to path, signaling that Walk
-// will not walk into that directory. The function can decide how to
-// handle that error; as described earlier, returning the error will
-// cause Walk to stop walking the entire tree.
-//
-// Walk calls the function with a non-nil err argument in two cases.
-//
-// First, if an os.Lstat on the root directory or any directory or file
-// in the tree fails, Walk calls the function with path set to that
-// directory or file's path, info set to nil, and err set to the error
-// from os.Lstat.
-//
-// Second, if a directory's Readdirnames method fails, Walk calls the
-// function with path set to the directory's path, info, set to an
-// fs.FileInfo describing the directory, and err set to the error from
-// Readdirnames.
-type WalkFunc func(path string, info fs.FileInfo, err error) error
-
-var lstat = os.Lstat // for testing
-
-// walkDir recursively descends path, calling walkDirFn.
-func walkDir(path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
-	if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
-		if err == SkipDir && d.IsDir() {
-			// Successfully skipped directory.
-			err = nil
-		}
-		return err
-	}
-
-	dirs, err := readDir(path)
-	if err != nil {
-		// Second call, to report ReadDir error.
-		err = walkDirFn(path, d, err)
-		if err != nil {
-			return err
-		}
-	}
-
-	for _, d1 := range dirs {
-		path1 := Join(path, d1.Name())
-		if err := walkDir(path1, d1, walkDirFn); err != nil {
-			if err == SkipDir {
-				break
-			}
-			return err
-		}
-	}
-	return nil
-}
-
-// walk recursively descends path, calling walkFn.
-func walk(path string, info fs.FileInfo, walkFn WalkFunc) error {
-	if !info.IsDir() {
-		return walkFn(path, info, nil)
-	}
-
-	names, err := readDirNames(path)
-	err1 := walkFn(path, info, err)
-	// If err != nil, walk can't walk into this directory.
-	// err1 != nil means walkFn want walk to skip this directory or stop walking.
-	// Therefore, if one of err and err1 isn't nil, walk will return.
-	if err != nil || err1 != nil {
-		// The caller's behavior is controlled by the return value, which is decided
-		// by walkFn. walkFn may ignore err and return nil.
-		// If walkFn returns SkipDir, it will be handled by the caller.
-		// So walk should return whatever walkFn returns.
-		return err1
-	}
-
-	for _, name := range names {
-		filename := Join(path, name)
-		fileInfo, err := lstat(filename)
-		if err != nil {
-			if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
-				return err
-			}
-		} else {
-			err = walk(filename, fileInfo, walkFn)
-			if err != nil {
-				if !fileInfo.IsDir() || err != SkipDir {
-					return err
-				}
-			}
-		}
-	}
-	return nil
-}
-
-// WalkDir walks the file tree rooted at root, calling fn for each file or
-// directory in the tree, including root.
-//
-// All errors that arise visiting files and directories are filtered by fn:
-// see the fs.WalkDirFunc documentation for details.
-//
-// The files are walked in lexical order, which makes the output deterministic
-// but requires WalkDir to read an entire directory into memory before proceeding
-// to walk that directory.
-//
-// WalkDir does not follow symbolic links.
-func WalkDir(root string, fn fs.WalkDirFunc) error {
-	info, err := os.Lstat(root)
-	if err != nil {
-		err = fn(root, nil, err)
-	} else {
-		err = walkDir(root, &statDirEntry{info}, fn)
-	}
-	if err == SkipDir {
-		return nil
-	}
-	return err
-}
-
-type statDirEntry struct {
-	info fs.FileInfo
-}
-
-func (d *statDirEntry) Name() string               { return d.info.Name() }
-func (d *statDirEntry) IsDir() bool                { return d.info.IsDir() }
-func (d *statDirEntry) Type() fs.FileMode          { return d.info.Mode().Type() }
-func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
-
-// Walk walks the file tree rooted at root, calling fn for each file or
-// directory in the tree, including root.
-//
-// All errors that arise visiting files and directories are filtered by fn:
-// see the WalkFunc documentation for details.
-//
-// The files are walked in lexical order, which makes the output deterministic
-// but requires Walk to read an entire directory into memory before proceeding
-// to walk that directory.
-//
-// Walk does not follow symbolic links.
-//
-// Walk is less efficient than WalkDir, introduced in Go 1.16,
-// which avoids calling os.Lstat on every visited file or directory.
-func Walk(root string, fn WalkFunc) error {
-	info, err := os.Lstat(root)
-	if err != nil {
-		err = fn(root, nil, err)
-	} else {
-		err = walk(root, info, fn)
-	}
-	if err == SkipDir {
-		return nil
-	}
-	return err
-}
-
-// readDir reads the directory named by dirname and returns
-// a sorted list of directory entries.
-func readDir(dirname string) ([]fs.DirEntry, error) {
-	f, err := os.Open(dirname)
-	if err != nil {
-		return nil, err
-	}
-	dirs, err := f.ReadDir(-1)
-	f.Close()
-	if err != nil {
-		return nil, err
-	}
-	sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
-	return dirs, nil
-}
-
-// readDirNames reads the directory named by dirname and returns
-// a sorted list of directory entry names.
-func readDirNames(dirname string) ([]string, error) {
-	f, err := os.Open(dirname)
-	if err != nil {
-		return nil, err
-	}
-	names, err := f.Readdirnames(-1)
-	f.Close()
-	if err != nil {
-		return nil, err
-	}
-	sort.Strings(names)
-	return names, 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.
-func Base(path string) string {
+// 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 && os.IsPathSeparator(path[len(path)-1]) {
+	for len(path) > 0 && x.IsPathSeparator(path[len(path)-1]) {
 		path = path[0 : len(path)-1]
 	}
 	// Throw away volume name
-	path = path[len(VolumeName(path)):]
+	path = path[x.volumeNameLen(path):]
 	// Find the last element
 	i := len(path) - 1
-	for i >= 0 && !os.IsPathSeparator(path[i]) {
+	for i >= 0 && !x.IsPathSeparator(path[i]) {
 		i--
 	}
 	if i >= 0 {
@@ -562,7 +368,7 @@
 	}
 	// If empty now, it had only slashes.
 	if path == "" {
-		return string(Separator)
+		return string(x.Separator)
 	}
 	return path
 }
@@ -573,13 +379,15 @@
 // 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.
-func Dir(path string) string {
-	vol := VolumeName(path)
+// 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) && !os.IsPathSeparator(path[i]) {
+	for i >= len(vol) && !x.IsPathSeparator(path[i]) {
 		i--
 	}
-	dir := Clean(path[len(vol) : i+1])
+	dir := clean(path[len(vol):i+1], x)
 	if dir == "." && len(vol) > 2 {
 		// must be UNC
 		return vol
@@ -587,10 +395,20 @@
 	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 "".
-func VolumeName(path string) string {
-	return path[:volumeNameLen(path)]
+// 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/testdata/path_nix.go
index ec497d9..eb1b193 100644
--- a/pkg/path/testdata/path_nix.go
+++ b/pkg/path/testdata/path_nix.go
@@ -1,21 +1,46 @@
+// 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 2010 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.
 
-// +build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solaris
-
-package filepath
+package path
 
 import "strings"
 
+type unixInfo struct{}
+
+var _ osInfo = unixInfo{}
+
+const (
+	unixListSeparator = ':'
+	unixSeparator     = '/'
+)
+
+func (o unixInfo) IsPathSeparator(b byte) bool {
+	return b == unixSeparator
+}
+
 // IsAbs reports whether the path is absolute.
-func IsAbs(path string) bool {
+func (o unixInfo) IsAbs(path string) bool {
 	return strings.HasPrefix(path, "/")
 }
 
 // volumeNameLen returns length of the leading volume name on Windows.
 // It returns 0 elsewhere.
-func volumeNameLen(path string) int {
+func (o unixInfo) volumeNameLen(path string) int {
 	return 0
 }
 
@@ -23,31 +48,27 @@
 //
 // Deprecated: HasPrefix does not respect path boundaries and
 // does not ignore case when required.
-func HasPrefix(p, prefix string) bool {
+func (o unixInfo) HasPrefix(p, prefix string) bool {
 	return strings.HasPrefix(p, prefix)
 }
 
-func splitList(path string) []string {
+func (o unixInfo) splitList(path string) []string {
 	if path == "" {
 		return []string{}
 	}
-	return strings.Split(path, string(ListSeparator))
+	return strings.Split(path, string(unixListSeparator))
 }
 
-func abs(path string) (string, error) {
-	return unixAbs(path)
-}
-
-func join(elem []string) string {
+func (o unixInfo) join(elem []string) string {
 	// If there's a bug here, fix the logic in ./path_plan9.go too.
 	for i, e := range elem {
 		if e != "" {
-			return Clean(strings.Join(elem[i:], string(Separator)))
+			return clean(strings.Join(elem[i:], string(unixSeparator)), unix)
 		}
 	}
 	return ""
 }
 
-func sameWord(a, b string) bool {
+func (o unixInfo) sameWord(a, b string) bool {
 	return a == b
 }
diff --git a/pkg/path/testdata/path_p9.go b/pkg/path/testdata/path_p9.go
index ec792fc..f3379ea 100644
--- a/pkg/path/testdata/path_p9.go
+++ b/pkg/path/testdata/path_p9.go
@@ -1,19 +1,44 @@
+// 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 2010 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
+package path
 
 import "strings"
 
+const plan9Separator = '/'
+const plan9ListSeparator = '\000'
+
+type plan9Info struct{}
+
+var _ osInfo = plan9Info{}
+
+func (o plan9Info) IsPathSeparator(b byte) bool {
+	return b == plan9Separator
+}
+
 // IsAbs reports whether the path is absolute.
-func IsAbs(path string) bool {
+func (o plan9Info) IsAbs(path string) bool {
 	return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "#")
 }
 
 // volumeNameLen returns length of the leading volume name on Windows.
 // It returns 0 elsewhere.
-func volumeNameLen(path string) int {
+func (o plan9Info) volumeNameLen(path string) int {
 	return 0
 }
 
@@ -21,31 +46,27 @@
 //
 // Deprecated: HasPrefix does not respect path boundaries and
 // does not ignore case when required.
-func HasPrefix(p, prefix string) bool {
+func (o plan9Info) HasPrefix(p, prefix string) bool {
 	return strings.HasPrefix(p, prefix)
 }
 
-func splitList(path string) []string {
+func (o plan9Info) splitList(path string) []string {
 	if path == "" {
 		return []string{}
 	}
-	return strings.Split(path, string(ListSeparator))
+	return strings.Split(path, string(plan9ListSeparator))
 }
 
-func abs(path string) (string, error) {
-	return unixAbs(path)
-}
-
-func join(elem []string) string {
+func (o plan9Info) join(elem []string) string {
 	// If there's a bug here, fix the logic in ./path_unix.go too.
 	for i, e := range elem {
 		if e != "" {
-			return Clean(strings.Join(elem[i:], string(Separator)))
+			return clean(strings.Join(elem[i:], string(plan9Separator)), plan9)
 		}
 	}
 	return ""
 }
 
-func sameWord(a, b string) bool {
+func (o plan9Info) sameWord(a, b string) bool {
 	return a == b
 }
diff --git a/pkg/path/testdata/path_test.go b/pkg/path/testdata/path_test.go
index d760530..61381c6 100644
--- a/pkg/path/testdata/path_test.go
+++ b/pkg/path/testdata/path_test.go
@@ -1,22 +1,26 @@
+// 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_test
+package path
 
 import (
-	"errors"
-	"fmt"
-	"internal/testenv"
-	"io/fs"
-	"io/ioutil"
-	"os"
-	"path/filepath"
 	"reflect"
 	"runtime"
-	"sort"
-	"strings"
-	"syscall"
 	"testing"
 )
 
@@ -98,53 +102,57 @@
 
 func TestClean(t *testing.T) {
 	tests := cleantests
-	if runtime.GOOS == "windows" {
-		for i := range tests {
-			tests[i].result = filepath.FromSlash(tests[i].result)
+	for _, os := range []OS{Unix, Windows, Plan9} {
+		if os == Windows {
+			for i := range tests {
+				tests[i].result = FromSlash(tests[i].result, os)
+			}
+			tests = append(tests, wincleantests...)
 		}
-		tests = append(tests, wincleantests...)
-	}
-	for _, test := range tests {
-		if s := filepath.Clean(test.path); s != test.result {
-			t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.result)
+		for _, test := range tests {
+			if s := Clean(test.path, os); s != test.result {
+				t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.result)
+			}
+			if s := Clean(test.result, os); s != test.result {
+				t.Errorf("Clean(%q) = %q, want %q", test.result, s, test.result)
+			}
 		}
-		if s := filepath.Clean(test.result); s != test.result {
-			t.Errorf("Clean(%q) = %q, want %q", test.result, s, test.result)
+
+		if testing.Short() {
+			t.Skip("skipping malloc count in short mode")
+		}
+		if runtime.GOMAXPROCS(0) > 1 {
+			t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
+			return
+		}
+
+		for _, test := range tests {
+			allocs := testing.AllocsPerRun(100, func() { Clean(test.result, os) })
+			if allocs > 0 {
+				t.Errorf("Clean(%q): %v allocs, want zero", test.result, allocs)
+			}
 		}
 	}
-
-	if testing.Short() {
-		t.Skip("skipping malloc count in short mode")
-	}
-	if runtime.GOMAXPROCS(0) > 1 {
-		t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
-		return
-	}
-
-	for _, test := range tests {
-		allocs := testing.AllocsPerRun(100, func() { filepath.Clean(test.result) })
-		if allocs > 0 {
-			t.Errorf("Clean(%q): %v allocs, want zero", test.result, allocs)
-		}
-	}
-}
-
-const sep = filepath.Separator
-
-var slashtests = []PathTest{
-	{"", ""},
-	{"/", string(sep)},
-	{"/a/b", string([]byte{sep, 'a', sep, 'b'})},
-	{"a//b", string([]byte{'a', sep, sep, 'b'})},
 }
 
 func TestFromAndToSlash(t *testing.T) {
-	for _, test := range slashtests {
-		if s := filepath.FromSlash(test.path); s != test.result {
-			t.Errorf("FromSlash(%q) = %q, want %q", test.path, s, test.result)
+	for _, o := range []OS{Unix, Windows, Plan9} {
+		sep := getOS(o).Separator
+
+		var slashtests = []PathTest{
+			{"", ""},
+			{"/", string(sep)},
+			{"/a/b", string([]byte{sep, 'a', sep, 'b'})},
+			{"a//b", string([]byte{'a', sep, sep, 'b'})},
 		}
-		if s := filepath.ToSlash(test.result); s != test.path {
-			t.Errorf("ToSlash(%q) = %q, want %q", test.result, s, test.path)
+
+		for _, test := range slashtests {
+			if s := FromSlash(test.path, o); s != test.result {
+				t.Errorf("FromSlash(%q) = %q, want %q", test.path, s, test.result)
+			}
+			if s := ToSlash(test.result, o); s != test.path {
+				t.Errorf("ToSlash(%q) = %q, want %q", test.result, s, test.path)
+			}
 		}
 	}
 }
@@ -154,14 +162,6 @@
 	result []string
 }
 
-const lsep = filepath.ListSeparator
-
-var splitlisttests = []SplitListTest{
-	{"", []string{}},
-	{string([]byte{'a', lsep, 'b'}), []string{"a", "b"}},
-	{string([]byte{lsep, 'a', lsep, 'b'}), []string{"", "a", "b"}},
-}
-
 var winsplitlisttests = []SplitListTest{
 	// quoted
 	{`"a"`, []string{`a`}},
@@ -185,13 +185,21 @@
 }
 
 func TestSplitList(t *testing.T) {
-	tests := splitlisttests
-	if runtime.GOOS == "windows" {
-		tests = append(tests, winsplitlisttests...)
-	}
-	for _, test := range tests {
-		if l := filepath.SplitList(test.list); !reflect.DeepEqual(l, test.result) {
-			t.Errorf("SplitList(%#q) = %#q, want %#q", test.list, l, test.result)
+	for _, os := range []OS{Unix, Windows, Plan9} {
+		sep := getOS(os).ListSeparator
+
+		tests := []SplitListTest{
+			{"", []string{}},
+			{string([]byte{'a', sep, 'b'}), []string{"a", "b"}},
+			{string([]byte{sep, 'a', sep, 'b'}), []string{"", "a", "b"}},
+		}
+		if os == Windows {
+			tests = append(tests, winsplitlisttests...)
+		}
+		for _, test := range tests {
+			if l := SplitList(test.list, os); !reflect.DeepEqual(l, test.result) {
+				t.Errorf("SplitList(%#q, %q) = %#q, want %#q", test.list, os, l, test.result)
+			}
 		}
 	}
 }
@@ -222,14 +230,19 @@
 }
 
 func TestSplit(t *testing.T) {
-	var splittests []SplitTest
-	splittests = unixsplittests
-	if runtime.GOOS == "windows" {
-		splittests = append(splittests, winsplittests...)
-	}
-	for _, test := range splittests {
-		if d, f := filepath.Split(test.path); d != test.dir || f != test.file {
-			t.Errorf("Split(%q) = %q, %q, want %q, %q", test.path, d, f, test.dir, test.file)
+	for _, os := range []OS{Windows, Unix} {
+		var splittests []SplitTest
+		splittests = unixsplittests
+		if os == Windows {
+			splittests = append(splittests, winsplittests...)
+		}
+		for _, test := range splittests {
+			pair := Split(test.path, os)
+			d, f := pair[0], pair[1]
+			if d != test.dir || f != test.file {
+				t.Errorf("Split(%q, %q) = %q, %q, want %q, %q",
+					test.path, os, d, f, test.dir, test.file)
+			}
 		}
 	}
 }
@@ -295,13 +308,15 @@
 }
 
 func TestJoin(t *testing.T) {
-	if runtime.GOOS == "windows" {
-		jointests = append(jointests, winjointests...)
-	}
-	for _, test := range jointests {
-		expected := filepath.FromSlash(test.path)
-		if p := filepath.Join(test.elem...); p != expected {
-			t.Errorf("join(%q) = %q, want %q", test.elem, p, expected)
+	for _, os := range []OS{Unix, Windows} {
+		if os == Windows {
+			jointests = append(jointests, winjointests...)
+		}
+		for _, test := range jointests {
+			expected := FromSlash(test.path, os)
+			if p := Join(test.elem, os); p != expected {
+				t.Errorf("join(%q, %q) = %q, want %q", test.elem, os, p, expected)
+			}
 		}
 	}
 }
@@ -319,343 +334,12 @@
 }
 
 func TestExt(t *testing.T) {
-	for _, test := range exttests {
-		if x := filepath.Ext(test.path); x != test.ext {
-			t.Errorf("Ext(%q) = %q, want %q", test.path, x, test.ext)
-		}
-	}
-}
-
-type Node struct {
-	name    string
-	entries []*Node // nil if the entry is a file
-	mark    int
-}
-
-var tree = &Node{
-	"testdata",
-	[]*Node{
-		{"a", nil, 0},
-		{"b", []*Node{}, 0},
-		{"c", nil, 0},
-		{
-			"d",
-			[]*Node{
-				{"x", nil, 0},
-				{"y", []*Node{}, 0},
-				{
-					"z",
-					[]*Node{
-						{"u", nil, 0},
-						{"v", nil, 0},
-					},
-					0,
-				},
-			},
-			0,
-		},
-	},
-	0,
-}
-
-func walkTree(n *Node, path string, f func(path string, n *Node)) {
-	f(path, n)
-	for _, e := range n.entries {
-		walkTree(e, filepath.Join(path, e.name), f)
-	}
-}
-
-func makeTree(t *testing.T) {
-	walkTree(tree, tree.name, func(path string, n *Node) {
-		if n.entries == nil {
-			fd, err := os.Create(path)
-			if err != nil {
-				t.Errorf("makeTree: %v", err)
-				return
+	for _, os := range []OS{Unix, Windows} {
+		for _, test := range exttests {
+			if x := Ext(test.path, os); x != test.ext {
+				t.Errorf("Ext(%q, %q) = %q, want %q", test.path, os, x, test.ext)
 			}
-			fd.Close()
-		} else {
-			os.Mkdir(path, 0770)
 		}
-	})
-}
-
-func markTree(n *Node) { walkTree(n, "", func(path string, n *Node) { n.mark++ }) }
-
-func checkMarks(t *testing.T, report bool) {
-	walkTree(tree, tree.name, func(path string, n *Node) {
-		if n.mark != 1 && report {
-			t.Errorf("node %s mark = %d; expected 1", path, n.mark)
-		}
-		n.mark = 0
-	})
-}
-
-// Assumes that each node name is unique. Good enough for a test.
-// If clear is true, any incoming error is cleared before return. The errors
-// are always accumulated, though.
-func mark(d fs.DirEntry, err error, errors *[]error, clear bool) error {
-	name := d.Name()
-	walkTree(tree, tree.name, func(path string, n *Node) {
-		if n.name == name {
-			n.mark++
-		}
-	})
-	if err != nil {
-		*errors = append(*errors, err)
-		if clear {
-			return nil
-		}
-		return err
-	}
-	return nil
-}
-
-func chtmpdir(t *testing.T) (restore func()) {
-	oldwd, err := os.Getwd()
-	if err != nil {
-		t.Fatalf("chtmpdir: %v", err)
-	}
-	d, err := ioutil.TempDir("", "test")
-	if err != nil {
-		t.Fatalf("chtmpdir: %v", err)
-	}
-	if err := os.Chdir(d); err != nil {
-		t.Fatalf("chtmpdir: %v", err)
-	}
-	return func() {
-		if err := os.Chdir(oldwd); err != nil {
-			t.Fatalf("chtmpdir: %v", err)
-		}
-		os.RemoveAll(d)
-	}
-}
-
-func TestWalk(t *testing.T) {
-	walk := func(root string, fn fs.WalkDirFunc) error {
-		return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
-			return fn(path, &statDirEntry{info}, err)
-		})
-	}
-	testWalk(t, walk, 1)
-}
-
-type statDirEntry struct {
-	info fs.FileInfo
-}
-
-func (d *statDirEntry) Name() string               { return d.info.Name() }
-func (d *statDirEntry) IsDir() bool                { return d.info.IsDir() }
-func (d *statDirEntry) Type() fs.FileMode          { return d.info.Mode().Type() }
-func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
-
-func TestWalkDir(t *testing.T) {
-	testWalk(t, filepath.WalkDir, 2)
-}
-
-func testWalk(t *testing.T, walk func(string, fs.WalkDirFunc) error, errVisit int) {
-	if runtime.GOOS == "ios" {
-		restore := chtmpdir(t)
-		defer restore()
-	}
-
-	tmpDir, err := ioutil.TempDir("", "TestWalk")
-	if err != nil {
-		t.Fatal("creating temp dir:", err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	origDir, err := os.Getwd()
-	if err != nil {
-		t.Fatal("finding working dir:", err)
-	}
-	if err = os.Chdir(tmpDir); err != nil {
-		t.Fatal("entering temp dir:", err)
-	}
-	defer os.Chdir(origDir)
-
-	makeTree(t)
-	errors := make([]error, 0, 10)
-	clear := true
-	markFn := func(path string, d fs.DirEntry, err error) error {
-		return mark(d, err, &errors, clear)
-	}
-	// Expect no errors.
-	err = walk(tree.name, markFn)
-	if err != nil {
-		t.Fatalf("no error expected, found: %s", err)
-	}
-	if len(errors) != 0 {
-		t.Fatalf("unexpected errors: %s", errors)
-	}
-	checkMarks(t, true)
-	errors = errors[0:0]
-
-	t.Run("PermErr", func(t *testing.T) {
-		// Test permission errors. Only possible if we're not root
-		// and only on some file systems (AFS, FAT).  To avoid errors during
-		// all.bash on those file systems, skip during go test -short.
-		if runtime.GOOS == "windows" {
-			t.Skip("skipping on Windows")
-		}
-		if os.Getuid() == 0 {
-			t.Skip("skipping as root")
-		}
-		if testing.Short() {
-			t.Skip("skipping in short mode")
-		}
-
-		// introduce 2 errors: chmod top-level directories to 0
-		os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0)
-		os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0)
-
-		// 3) capture errors, expect two.
-		// mark respective subtrees manually
-		markTree(tree.entries[1])
-		markTree(tree.entries[3])
-		// correct double-marking of directory itself
-		tree.entries[1].mark -= errVisit
-		tree.entries[3].mark -= errVisit
-		err := walk(tree.name, markFn)
-		if err != nil {
-			t.Fatalf("expected no error return from Walk, got %s", err)
-		}
-		if len(errors) != 2 {
-			t.Errorf("expected 2 errors, got %d: %s", len(errors), errors)
-		}
-		// the inaccessible subtrees were marked manually
-		checkMarks(t, true)
-		errors = errors[0:0]
-
-		// 4) capture errors, stop after first error.
-		// mark respective subtrees manually
-		markTree(tree.entries[1])
-		markTree(tree.entries[3])
-		// correct double-marking of directory itself
-		tree.entries[1].mark -= errVisit
-		tree.entries[3].mark -= errVisit
-		clear = false // error will stop processing
-		err = walk(tree.name, markFn)
-		if err == nil {
-			t.Fatalf("expected error return from Walk")
-		}
-		if len(errors) != 1 {
-			t.Errorf("expected 1 error, got %d: %s", len(errors), errors)
-		}
-		// the inaccessible subtrees were marked manually
-		checkMarks(t, false)
-		errors = errors[0:0]
-
-		// restore permissions
-		os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0770)
-		os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0770)
-	})
-}
-
-func touch(t *testing.T, name string) {
-	f, err := os.Create(name)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err := f.Close(); err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestWalkSkipDirOnFile(t *testing.T) {
-	td, err := ioutil.TempDir("", "walktest")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(td)
-
-	if err := os.MkdirAll(filepath.Join(td, "dir"), 0755); err != nil {
-		t.Fatal(err)
-	}
-	touch(t, filepath.Join(td, "dir/foo1"))
-	touch(t, filepath.Join(td, "dir/foo2"))
-
-	sawFoo2 := false
-	walker := func(path string) error {
-		if strings.HasSuffix(path, "foo2") {
-			sawFoo2 = true
-		}
-		if strings.HasSuffix(path, "foo1") {
-			return filepath.SkipDir
-		}
-		return nil
-	}
-	walkFn := func(path string, _ fs.FileInfo, _ error) error { return walker(path) }
-	walkDirFn := func(path string, _ fs.DirEntry, _ error) error { return walker(path) }
-
-	check := func(t *testing.T, walk func(root string) error, root string) {
-		t.Helper()
-		sawFoo2 = false
-		err = walk(root)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if sawFoo2 {
-			t.Errorf("SkipDir on file foo1 did not block processing of foo2")
-		}
-	}
-
-	t.Run("Walk", func(t *testing.T) {
-		Walk := func(root string) error { return filepath.Walk(td, walkFn) }
-		check(t, Walk, td)
-		check(t, Walk, filepath.Join(td, "dir"))
-	})
-	t.Run("WalkDir", func(t *testing.T) {
-		WalkDir := func(root string) error { return filepath.WalkDir(td, walkDirFn) }
-		check(t, WalkDir, td)
-		check(t, WalkDir, filepath.Join(td, "dir"))
-	})
-}
-
-func TestWalkFileError(t *testing.T) {
-	td, err := ioutil.TempDir("", "walktest")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(td)
-
-	touch(t, filepath.Join(td, "foo"))
-	touch(t, filepath.Join(td, "bar"))
-	dir := filepath.Join(td, "dir")
-	if err := os.MkdirAll(filepath.Join(td, "dir"), 0755); err != nil {
-		t.Fatal(err)
-	}
-	touch(t, filepath.Join(dir, "baz"))
-	touch(t, filepath.Join(dir, "stat-error"))
-	defer func() {
-		*filepath.LstatP = os.Lstat
-	}()
-	statErr := errors.New("some stat error")
-	*filepath.LstatP = func(path string) (fs.FileInfo, error) {
-		if strings.HasSuffix(path, "stat-error") {
-			return nil, statErr
-		}
-		return os.Lstat(path)
-	}
-	got := map[string]error{}
-	err = filepath.Walk(td, func(path string, fi fs.FileInfo, err error) error {
-		rel, _ := filepath.Rel(td, path)
-		got[filepath.ToSlash(rel)] = err
-		return nil
-	})
-	if err != nil {
-		t.Errorf("Walk error: %v", err)
-	}
-	want := map[string]error{
-		".":              nil,
-		"foo":            nil,
-		"bar":            nil,
-		"dir":            nil,
-		"dir/baz":        nil,
-		"dir/stat-error": statErr,
-	}
-	if !reflect.DeepEqual(got, want) {
-		t.Errorf("Walked %#v; want %#v", got, want)
 	}
 }
 
@@ -686,17 +370,19 @@
 
 func TestBase(t *testing.T) {
 	tests := basetests
-	if runtime.GOOS == "windows" {
-		// make unix tests work on windows
-		for i := range tests {
-			tests[i].result = filepath.Clean(tests[i].result)
+	for _, os := range []OS{Unix, Windows} {
+		if os == Windows {
+			// make unix tests work on windows
+			for i := range tests {
+				tests[i].result = Clean(tests[i].result, os)
+			}
+			// add windows specific tests
+			tests = append(tests, winbasetests...)
 		}
-		// add windows specific tests
-		tests = append(tests, winbasetests...)
-	}
-	for _, test := range tests {
-		if s := filepath.Base(test.path); s != test.result {
-			t.Errorf("Base(%q) = %q, want %q", test.path, s, test.result)
+		for _, test := range tests {
+			if s := Base(test.path, os); s != test.result {
+				t.Errorf("Base(%q, %q) = %q, want %q", test.path, os, s, test.result)
+			}
 		}
 	}
 }
@@ -729,18 +415,20 @@
 }
 
 func TestDir(t *testing.T) {
-	tests := dirtests
-	if runtime.GOOS == "windows" {
-		// make unix tests work on windows
-		for i := range tests {
-			tests[i].result = filepath.Clean(tests[i].result)
+	for _, os := range []OS{Unix, Windows} {
+		tests := dirtests
+		if os == Windows {
+			// make unix tests work on windows
+			for i := range tests {
+				tests[i].result = Clean(tests[i].result, os)
+			}
+			// add windows specific tests
+			tests = append(tests, windirtests...)
 		}
-		// add windows specific tests
-		tests = append(tests, windirtests...)
-	}
-	for _, test := range tests {
-		if s := filepath.Dir(test.path); s != test.result {
-			t.Errorf("Dir(%q) = %q, want %q", test.path, s, test.result)
+		for _, test := range tests {
+			if s := Dir(test.path, os); s != test.result {
+				t.Errorf("Dir(%q, %q) = %q, want %q", test.path, os, s, test.result)
+			}
 		}
 	}
 }
@@ -777,267 +465,41 @@
 }
 
 func TestIsAbs(t *testing.T) {
-	var tests []IsAbsTest
-	if runtime.GOOS == "windows" {
-		tests = append(tests, winisabstests...)
-		// All non-windows tests should fail, because they have no volume letter.
-		for _, test := range isabstests {
-			tests = append(tests, IsAbsTest{test.path, false})
-		}
-		// All non-windows test should work as intended if prefixed with volume letter.
-		for _, test := range isabstests {
-			tests = append(tests, IsAbsTest{"c:" + test.path, test.isAbs})
-		}
-		// Test reserved names.
-		tests = append(tests, IsAbsTest{os.DevNull, true})
-		tests = append(tests, IsAbsTest{"NUL", true})
-		tests = append(tests, IsAbsTest{"nul", true})
-		tests = append(tests, IsAbsTest{"CON", true})
-	} else {
-		tests = isabstests
-	}
-
-	for _, test := range tests {
-		if r := filepath.IsAbs(test.path); r != test.isAbs {
-			t.Errorf("IsAbs(%q) = %v, want %v", test.path, r, test.isAbs)
-		}
-	}
-}
-
-type EvalSymlinksTest struct {
-	// If dest is empty, the path is created; otherwise the dest is symlinked to the path.
-	path, dest string
-}
-
-var EvalSymlinksTestDirs = []EvalSymlinksTest{
-	{"test", ""},
-	{"test/dir", ""},
-	{"test/dir/link3", "../../"},
-	{"test/link1", "../test"},
-	{"test/link2", "dir"},
-	{"test/linkabs", "/"},
-	{"test/link4", "../test2"},
-	{"test2", "test/dir"},
-	// Issue 23444.
-	{"src", ""},
-	{"src/pool", ""},
-	{"src/pool/test", ""},
-	{"src/versions", ""},
-	{"src/versions/current", "../../version"},
-	{"src/versions/v1", ""},
-	{"src/versions/v1/modules", ""},
-	{"src/versions/v1/modules/test", "../../../pool/test"},
-	{"version", "src/versions/v1"},
-}
-
-var EvalSymlinksTests = []EvalSymlinksTest{
-	{"test", "test"},
-	{"test/dir", "test/dir"},
-	{"test/dir/../..", "."},
-	{"test/link1", "test"},
-	{"test/link2", "test/dir"},
-	{"test/link1/dir", "test/dir"},
-	{"test/link2/..", "test"},
-	{"test/dir/link3", "."},
-	{"test/link2/link3/test", "test"},
-	{"test/linkabs", "/"},
-	{"test/link4/..", "test"},
-	{"src/versions/current/modules/test", "src/pool/test"},
-}
-
-// simpleJoin builds a file name from the directory and path.
-// It does not use Join because we don't want ".." to be evaluated.
-func simpleJoin(dir, path string) string {
-	return dir + string(filepath.Separator) + path
-}
-
-func testEvalSymlinks(t *testing.T, path, want string) {
-	have, err := filepath.EvalSymlinks(path)
-	if err != nil {
-		t.Errorf("EvalSymlinks(%q) error: %v", path, err)
-		return
-	}
-	if filepath.Clean(have) != filepath.Clean(want) {
-		t.Errorf("EvalSymlinks(%q) returns %q, want %q", path, have, want)
-	}
-}
-
-func testEvalSymlinksAfterChdir(t *testing.T, wd, path, want string) {
-	cwd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		err := os.Chdir(cwd)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}()
-
-	err = os.Chdir(wd)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	have, err := filepath.EvalSymlinks(path)
-	if err != nil {
-		t.Errorf("EvalSymlinks(%q) in %q directory error: %v", path, wd, err)
-		return
-	}
-	if filepath.Clean(have) != filepath.Clean(want) {
-		t.Errorf("EvalSymlinks(%q) in %q directory returns %q, want %q", path, wd, have, want)
-	}
-}
-
-func TestEvalSymlinks(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	tmpDir, err := ioutil.TempDir("", "evalsymlink")
-	if err != nil {
-		t.Fatal("creating temp dir:", err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	// /tmp may itself be a symlink! Avoid the confusion, although
-	// it means trusting the thing we're testing.
-	tmpDir, err = filepath.EvalSymlinks(tmpDir)
-	if err != nil {
-		t.Fatal("eval symlink for tmp dir:", err)
-	}
-
-	// Create the symlink farm using relative paths.
-	for _, d := range EvalSymlinksTestDirs {
-		var err error
-		path := simpleJoin(tmpDir, d.path)
-		if d.dest == "" {
-			err = os.Mkdir(path, 0755)
+	for _, os := range []OS{Unix, Windows} {
+		var tests []IsAbsTest
+		if os == Windows {
+			tests = append(tests, winisabstests...)
+			// All non-windows tests should fail, because they have no volume letter.
+			for _, test := range isabstests {
+				tests = append(tests, IsAbsTest{test.path, false})
+			}
+			// All non-windows test should work as intended if prefixed with volume letter.
+			for _, test := range isabstests {
+				tests = append(tests, IsAbsTest{"c:" + test.path, test.isAbs})
+			}
+			// Test reserved names.
+			// tests = append(tests, IsAbsTest{"/dev/null", true})
+			tests = append(tests, IsAbsTest{"NUL", true})
+			tests = append(tests, IsAbsTest{"nul", true})
+			tests = append(tests, IsAbsTest{"CON", true})
 		} else {
-			err = os.Symlink(d.dest, path)
-		}
-		if err != nil {
-			t.Fatal(err)
-		}
-	}
-
-	// Evaluate the symlink farm.
-	for _, test := range EvalSymlinksTests {
-		path := simpleJoin(tmpDir, test.path)
-
-		dest := simpleJoin(tmpDir, test.dest)
-		if filepath.IsAbs(test.dest) || os.IsPathSeparator(test.dest[0]) {
-			dest = test.dest
-		}
-		testEvalSymlinks(t, path, dest)
-
-		// test EvalSymlinks(".")
-		testEvalSymlinksAfterChdir(t, path, ".", ".")
-
-		// test EvalSymlinks("C:.") on Windows
-		if runtime.GOOS == "windows" {
-			volDot := filepath.VolumeName(tmpDir) + "."
-			testEvalSymlinksAfterChdir(t, path, volDot, volDot)
+			tests = isabstests
 		}
 
-		// test EvalSymlinks(".."+path)
-		dotdotPath := simpleJoin("..", test.dest)
-		if filepath.IsAbs(test.dest) || os.IsPathSeparator(test.dest[0]) {
-			dotdotPath = test.dest
-		}
-		testEvalSymlinksAfterChdir(t,
-			simpleJoin(tmpDir, "test"),
-			simpleJoin("..", test.path),
-			dotdotPath)
-
-		// test EvalSymlinks(p) where p is relative path
-		testEvalSymlinksAfterChdir(t, tmpDir, test.path, test.dest)
-	}
-}
-
-func TestEvalSymlinksIsNotExist(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	defer chtmpdir(t)()
-
-	_, err := filepath.EvalSymlinks("notexist")
-	if !os.IsNotExist(err) {
-		t.Errorf("expected the file is not found, got %v\n", err)
-	}
-
-	err = os.Symlink("notexist", "link")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.Remove("link")
-
-	_, err = filepath.EvalSymlinks("link")
-	if !os.IsNotExist(err) {
-		t.Errorf("expected the file is not found, got %v\n", err)
-	}
-}
-
-func TestIssue13582(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	tmpDir, err := ioutil.TempDir("", "issue13582")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	dir := filepath.Join(tmpDir, "dir")
-	err = os.Mkdir(dir, 0755)
-	if err != nil {
-		t.Fatal(err)
-	}
-	linkToDir := filepath.Join(tmpDir, "link_to_dir")
-	err = os.Symlink(dir, linkToDir)
-	if err != nil {
-		t.Fatal(err)
-	}
-	file := filepath.Join(linkToDir, "file")
-	err = ioutil.WriteFile(file, nil, 0644)
-	if err != nil {
-		t.Fatal(err)
-	}
-	link1 := filepath.Join(linkToDir, "link1")
-	err = os.Symlink(file, link1)
-	if err != nil {
-		t.Fatal(err)
-	}
-	link2 := filepath.Join(linkToDir, "link2")
-	err = os.Symlink(link1, link2)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// /tmp may itself be a symlink!
-	realTmpDir, err := filepath.EvalSymlinks(tmpDir)
-	if err != nil {
-		t.Fatal(err)
-	}
-	realDir := filepath.Join(realTmpDir, "dir")
-	realFile := filepath.Join(realDir, "file")
-
-	tests := []struct {
-		path, want string
-	}{
-		{dir, realDir},
-		{linkToDir, realDir},
-		{file, realFile},
-		{link1, realFile},
-		{link2, realFile},
-	}
-	for i, test := range tests {
-		have, err := filepath.EvalSymlinks(test.path)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if have != test.want {
-			t.Errorf("test#%d: EvalSymlinks(%q) returns %q, want %q", i, test.path, have, test.want)
+		for _, test := range tests {
+			if r := IsAbs(test.path, os); r != test.isAbs {
+				t.Errorf("IsAbs(%q, %q) = %v, want %v", test.path, os, r, test.isAbs)
+			}
 		}
 	}
 }
 
+// // simpleJoin builds a file name from the directory and path.
+// // It does not use Join because we don't want ".." to be evaluated.
+// func simpleJoin(dir, path string) string {
+// 	return dir + string(Separator) + path
+// }
+
 // Test directories relative to temporary directory.
 // The tests are run in absTestDirs[0].
 var absTestDirs = []string{
@@ -1064,115 +526,6 @@
 	"$/a/b/c/../../.././a/",
 }
 
-func TestAbs(t *testing.T) {
-	root, err := ioutil.TempDir("", "TestAbs")
-	if err != nil {
-		t.Fatal("TempDir failed: ", err)
-	}
-	defer os.RemoveAll(root)
-
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal("getwd failed: ", err)
-	}
-	err = os.Chdir(root)
-	if err != nil {
-		t.Fatal("chdir failed: ", err)
-	}
-	defer os.Chdir(wd)
-
-	for _, dir := range absTestDirs {
-		err = os.Mkdir(dir, 0777)
-		if err != nil {
-			t.Fatal("Mkdir failed: ", err)
-		}
-	}
-
-	if runtime.GOOS == "windows" {
-		vol := filepath.VolumeName(root)
-		var extra []string
-		for _, path := range absTests {
-			if strings.Contains(path, "$") {
-				continue
-			}
-			path = vol + path
-			extra = append(extra, path)
-		}
-		absTests = append(absTests, extra...)
-	}
-
-	err = os.Chdir(absTestDirs[0])
-	if err != nil {
-		t.Fatal("chdir failed: ", err)
-	}
-
-	for _, path := range absTests {
-		path = strings.ReplaceAll(path, "$", root)
-		info, err := os.Stat(path)
-		if err != nil {
-			t.Errorf("%s: %s", path, err)
-			continue
-		}
-
-		abspath, err := filepath.Abs(path)
-		if err != nil {
-			t.Errorf("Abs(%q) error: %v", path, err)
-			continue
-		}
-		absinfo, err := os.Stat(abspath)
-		if err != nil || !os.SameFile(absinfo, info) {
-			t.Errorf("Abs(%q)=%q, not the same file", path, abspath)
-		}
-		if !filepath.IsAbs(abspath) {
-			t.Errorf("Abs(%q)=%q, not an absolute path", path, abspath)
-		}
-		if filepath.IsAbs(abspath) && abspath != filepath.Clean(abspath) {
-			t.Errorf("Abs(%q)=%q, isn't clean", path, abspath)
-		}
-	}
-}
-
-// Empty path needs to be special-cased on Windows. See golang.org/issue/24441.
-// We test it separately from all other absTests because the empty string is not
-// a valid path, so it can't be used with os.Stat.
-func TestAbsEmptyString(t *testing.T) {
-	root, err := ioutil.TempDir("", "TestAbsEmptyString")
-	if err != nil {
-		t.Fatal("TempDir failed: ", err)
-	}
-	defer os.RemoveAll(root)
-
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal("getwd failed: ", err)
-	}
-	err = os.Chdir(root)
-	if err != nil {
-		t.Fatal("chdir failed: ", err)
-	}
-	defer os.Chdir(wd)
-
-	info, err := os.Stat(root)
-	if err != nil {
-		t.Fatalf("%s: %s", root, err)
-	}
-
-	abspath, err := filepath.Abs("")
-	if err != nil {
-		t.Fatalf(`Abs("") error: %v`, err)
-	}
-	absinfo, err := os.Stat(abspath)
-	if err != nil || !os.SameFile(absinfo, info) {
-		t.Errorf(`Abs("")=%q, not the same file`, abspath)
-	}
-	if !filepath.IsAbs(abspath) {
-		t.Errorf(`Abs("")=%q, not an absolute path`, abspath)
-	}
-	if filepath.IsAbs(abspath) && abspath != filepath.Clean(abspath) {
-		t.Errorf(`Abs("")=%q, isn't clean`, abspath)
-	}
-}
-
 type RelTests struct {
 	root, path, want string
 }
@@ -1231,26 +584,28 @@
 }
 
 func TestRel(t *testing.T) {
-	tests := append([]RelTests{}, reltests...)
-	if runtime.GOOS == "windows" {
-		for i := range tests {
-			tests[i].want = filepath.FromSlash(tests[i].want)
-		}
-		tests = append(tests, winreltests...)
-	}
-	for _, test := range tests {
-		got, err := filepath.Rel(test.root, test.path)
-		if test.want == "err" {
-			if err == nil {
-				t.Errorf("Rel(%q, %q)=%q, want error", test.root, test.path, got)
+	for _, os := range []OS{Unix, Windows} {
+		tests := append([]RelTests{}, reltests...)
+		if os == Windows {
+			for i := range tests {
+				tests[i].want = FromSlash(tests[i].want, Windows)
 			}
-			continue
+			tests = append(tests, winreltests...)
 		}
-		if err != nil {
-			t.Errorf("Rel(%q, %q): want %q, got error: %s", test.root, test.path, test.want, err)
-		}
-		if got != test.want {
-			t.Errorf("Rel(%q, %q)=%q, want %q", test.root, test.path, got, test.want)
+		for _, test := range tests {
+			got, err := Rel(test.root, test.path, os)
+			if test.want == "err" {
+				if err == nil {
+					t.Errorf("Rel(%q, %q, %q)=%q, want error", test.root, test.path, os, got)
+				}
+				continue
+			}
+			if err != nil {
+				t.Errorf("Rel(%q, %q, %q): want %q, got error: %s", test.root, test.path, os, test.want, err)
+			}
+			if got != test.want {
+				t.Errorf("Rel(%q, %q, %q)=%q, want %q", test.root, test.path, os, got, test.want)
+			}
 		}
 	}
 }
@@ -1286,253 +641,14 @@
 }
 
 func TestVolumeName(t *testing.T) {
-	if runtime.GOOS != "windows" {
-		return
-	}
-	for _, v := range volumenametests {
-		if vol := filepath.VolumeName(v.path); vol != v.vol {
-			t.Errorf("VolumeName(%q)=%q, want %q", v.path, vol, v.vol)
+	for _, os := range []OS{Unix, Windows} {
+		if os != Windows {
+			return
 		}
-	}
-}
-
-func TestDriveLetterInEvalSymlinks(t *testing.T) {
-	if runtime.GOOS != "windows" {
-		return
-	}
-	wd, _ := os.Getwd()
-	if len(wd) < 3 {
-		t.Errorf("Current directory path %q is too short", wd)
-	}
-	lp := strings.ToLower(wd)
-	up := strings.ToUpper(wd)
-	flp, err := filepath.EvalSymlinks(lp)
-	if err != nil {
-		t.Fatalf("EvalSymlinks(%q) failed: %q", lp, err)
-	}
-	fup, err := filepath.EvalSymlinks(up)
-	if err != nil {
-		t.Fatalf("EvalSymlinks(%q) failed: %q", up, err)
-	}
-	if flp != fup {
-		t.Errorf("Results of EvalSymlinks do not match: %q and %q", flp, fup)
-	}
-}
-
-func TestBug3486(t *testing.T) { // https://golang.org/issue/3486
-	if runtime.GOOS == "ios" {
-		t.Skipf("skipping on %s/%s", runtime.GOOS, runtime.GOARCH)
-	}
-	root, err := filepath.EvalSymlinks(runtime.GOROOT() + "/test")
-	if err != nil {
-		t.Fatal(err)
-	}
-	bugs := filepath.Join(root, "fixedbugs")
-	ken := filepath.Join(root, "ken")
-	seenBugs := false
-	seenKen := false
-	err = filepath.Walk(root, func(pth string, info fs.FileInfo, err error) error {
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		switch pth {
-		case bugs:
-			seenBugs = true
-			return filepath.SkipDir
-		case ken:
-			if !seenBugs {
-				t.Fatal("filepath.Walk out of order - ken before fixedbugs")
+		for _, v := range volumenametests {
+			if vol := VolumeName(v.path, os); vol != v.vol {
+				t.Errorf("VolumeName(%q, %q)=%q, want %q", v.path, os, vol, v.vol)
 			}
-			seenKen = true
 		}
-		return nil
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	if !seenKen {
-		t.Fatalf("%q not seen", ken)
-	}
-}
-
-func testWalkSymlink(t *testing.T, mklink func(target, link string) error) {
-	tmpdir, err := ioutil.TempDir("", "testWalkSymlink")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpdir)
-
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.Chdir(wd)
-
-	err = os.Chdir(tmpdir)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = mklink(tmpdir, "link")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	var visited []string
-	err = filepath.Walk(tmpdir, func(path string, info fs.FileInfo, err error) error {
-		if err != nil {
-			t.Fatal(err)
-		}
-		rel, err := filepath.Rel(tmpdir, path)
-		if err != nil {
-			t.Fatal(err)
-		}
-		visited = append(visited, rel)
-		return nil
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	sort.Strings(visited)
-	want := []string{".", "link"}
-	if fmt.Sprintf("%q", visited) != fmt.Sprintf("%q", want) {
-		t.Errorf("unexpected paths visited %q, want %q", visited, want)
-	}
-}
-
-func TestWalkSymlink(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-	testWalkSymlink(t, os.Symlink)
-}
-
-func TestIssue29372(t *testing.T) {
-	tmpDir, err := ioutil.TempDir("", "TestIssue29372")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	path := filepath.Join(tmpDir, "file.txt")
-	err = ioutil.WriteFile(path, nil, 0644)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	pathSeparator := string(filepath.Separator)
-	tests := []string{
-		path + strings.Repeat(pathSeparator, 1),
-		path + strings.Repeat(pathSeparator, 2),
-		path + strings.Repeat(pathSeparator, 1) + ".",
-		path + strings.Repeat(pathSeparator, 2) + ".",
-		path + strings.Repeat(pathSeparator, 1) + "..",
-		path + strings.Repeat(pathSeparator, 2) + "..",
-	}
-
-	for i, test := range tests {
-		_, err = filepath.EvalSymlinks(test)
-		if err != syscall.ENOTDIR {
-			t.Fatalf("test#%d: want %q, got %q", i, syscall.ENOTDIR, err)
-		}
-	}
-}
-
-// Issue 30520 part 1.
-func TestEvalSymlinksAboveRoot(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	t.Parallel()
-
-	tmpDir, err := ioutil.TempDir("", "TestEvalSymlinksAboveRoot")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	evalTmpDir, err := filepath.EvalSymlinks(tmpDir)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if err := os.Mkdir(filepath.Join(evalTmpDir, "a"), 0777); err != nil {
-		t.Fatal(err)
-	}
-	if err := os.Symlink(filepath.Join(evalTmpDir, "a"), filepath.Join(evalTmpDir, "b")); err != nil {
-		t.Fatal(err)
-	}
-	if err := ioutil.WriteFile(filepath.Join(evalTmpDir, "a", "file"), nil, 0666); err != nil {
-		t.Fatal(err)
-	}
-
-	// Count the number of ".." elements to get to the root directory.
-	vol := filepath.VolumeName(evalTmpDir)
-	c := strings.Count(evalTmpDir[len(vol):], string(os.PathSeparator))
-	var dd []string
-	for i := 0; i < c+2; i++ {
-		dd = append(dd, "..")
-	}
-
-	wantSuffix := strings.Join([]string{"a", "file"}, string(os.PathSeparator))
-
-	// Try different numbers of "..".
-	for _, i := range []int{c, c + 1, c + 2} {
-		check := strings.Join([]string{evalTmpDir, strings.Join(dd[:i], string(os.PathSeparator)), evalTmpDir[len(vol)+1:], "b", "file"}, string(os.PathSeparator))
-		if resolved, err := filepath.EvalSymlinks(check); err != nil {
-			t.Errorf("EvalSymlinks(%q) failed: %v", check, err)
-		} else if !strings.HasSuffix(resolved, wantSuffix) {
-			t.Errorf("EvalSymlinks(%q) = %q does not end with %q", check, resolved, wantSuffix)
-		} else {
-			t.Logf("EvalSymlinks(%q) = %q", check, resolved)
-		}
-	}
-}
-
-// Issue 30520 part 2.
-func TestEvalSymlinksAboveRootChdir(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	tmpDir, err := ioutil.TempDir("", "TestEvalSymlinksAboveRootChdir")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.Chdir(wd)
-
-	if err := os.Chdir(tmpDir); err != nil {
-		t.Fatal(err)
-	}
-
-	subdir := filepath.Join("a", "b")
-	if err := os.MkdirAll(subdir, 0777); err != nil {
-		t.Fatal(err)
-	}
-	if err := os.Symlink(subdir, "c"); err != nil {
-		t.Fatal(err)
-	}
-	if err := ioutil.WriteFile(filepath.Join(subdir, "file"), nil, 0666); err != nil {
-		t.Fatal(err)
-	}
-
-	subdir = filepath.Join("d", "e", "f")
-	if err := os.MkdirAll(subdir, 0777); err != nil {
-		t.Fatal(err)
-	}
-	if err := os.Chdir(subdir); err != nil {
-		t.Fatal(err)
-	}
-
-	check := filepath.Join("..", "..", "..", "c", "file")
-	wantSuffix := filepath.Join("a", "b", "file")
-	if resolved, err := filepath.EvalSymlinks(check); err != nil {
-		t.Errorf("EvalSymlinks(%q) failed: %v", check, err)
-	} else if !strings.HasSuffix(resolved, wantSuffix) {
-		t.Errorf("EvalSymlinks(%q) = %q does not end with %q", check, resolved, wantSuffix)
-	} else {
-		t.Logf("EvalSymlinks(%q) = %q", check, resolved)
 	}
 }
diff --git a/pkg/path/testdata/path_win.go b/pkg/path/testdata/path_win.go
index 445c868..054ae28 100644
--- a/pkg/path/testdata/path_win.go
+++ b/pkg/path/testdata/path_win.go
@@ -1,18 +1,44 @@
+// 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 2010 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
+package path
 
 import (
 	"strings"
-	"syscall"
+)
+
+type windowsInfo struct{}
+
+var _ osInfo = windowsInfo{}
+
+const (
+	windowsSeparator     = '\\'
+	windowsListSeparator = ';'
 )
 
 func isSlash(c uint8) bool {
 	return c == '\\' || c == '/'
 }
 
+func (os windowsInfo) IsPathSeparator(b byte) bool {
+	return isSlash(b)
+}
+
 // reservedNames lists reserved Windows names. Search for PRN in
 // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
 // for details.
@@ -24,7 +50,7 @@
 
 // isReservedName returns true, if path is Windows reserved name.
 // See reservedNames for the full list.
-func isReservedName(path string) bool {
+func (os windowsInfo) isReservedName(path string) bool {
 	if len(path) == 0 {
 		return false
 	}
@@ -37,11 +63,11 @@
 }
 
 // IsAbs reports whether the path is absolute.
-func IsAbs(path string) (b bool) {
-	if isReservedName(path) {
+func (os windowsInfo) IsAbs(path string) (b bool) {
+	if os.isReservedName(path) {
 		return true
 	}
-	l := volumeNameLen(path)
+	l := os.volumeNameLen(path)
 	if l == 0 {
 		return false
 	}
@@ -54,7 +80,7 @@
 
 // volumeNameLen returns length of the leading volume name on Windows.
 // It returns 0 elsewhere.
-func volumeNameLen(path string) int {
+func (os windowsInfo) volumeNameLen(path string) int {
 	if len(path) < 2 {
 		return 0
 	}
@@ -94,14 +120,14 @@
 //
 // Deprecated: HasPrefix does not respect path boundaries and
 // does not ignore case when required.
-func HasPrefix(p, prefix string) bool {
+func (os windowsInfo) HasPrefix(p, prefix string) bool {
 	if strings.HasPrefix(p, prefix) {
 		return true
 	}
 	return strings.HasPrefix(strings.ToLower(p), strings.ToLower(prefix))
 }
 
-func splitList(path string) []string {
+func (os windowsInfo) splitList(path string) []string {
 	// The same implementation is used in LookPath in os/exec;
 	// consider changing os/exec when changing this.
 
@@ -117,7 +143,7 @@
 		switch c := path[i]; {
 		case c == '"':
 			quo = !quo
-		case c == ListSeparator && !quo:
+		case c == windowsListSeparator && !quo:
 			list = append(list, path[start:i])
 			start = i + 1
 		}
@@ -132,31 +158,17 @@
 	return list
 }
 
-func abs(path string) (string, error) {
-	if path == "" {
-		// syscall.FullPath returns an error on empty path, because it's not a valid path.
-		// To implement Abs behavior of returning working directory on empty string input,
-		// special-case empty path by changing it to "." path. See golang.org/issue/24441.
-		path = "."
-	}
-	fullPath, err := syscall.FullPath(path)
-	if err != nil {
-		return "", err
-	}
-	return Clean(fullPath), nil
-}
-
-func join(elem []string) string {
+func (os windowsInfo) join(elem []string) string {
 	for i, e := range elem {
 		if e != "" {
-			return joinNonEmpty(elem[i:])
+			return os.joinNonEmpty(elem[i:])
 		}
 	}
 	return ""
 }
 
 // joinNonEmpty is like join, but it assumes that the first element is non-empty.
-func joinNonEmpty(elem []string) string {
+func (o windowsInfo) joinNonEmpty(elem []string) string {
 	if len(elem[0]) == 2 && elem[0][1] == ':' {
 		// First element is drive letter without terminating slash.
 		// Keep path relative to current directory on that drive.
@@ -167,34 +179,34 @@
 				break
 			}
 		}
-		return Clean(elem[0] + strings.Join(elem[i:], string(Separator)))
+		return clean(elem[0]+strings.Join(elem[i:], string(windowsSeparator)), windows)
 	}
 	// The following logic prevents Join from inadvertently creating a
 	// UNC path on Windows. Unless the first element is a UNC path, Join
 	// shouldn't create a UNC path. See golang.org/issue/9167.
-	p := Clean(strings.Join(elem, string(Separator)))
+	p := clean(strings.Join(elem, string(windowsSeparator)), windows)
 	if !isUNC(p) {
 		return p
 	}
 	// p == UNC only allowed when the first element is a UNC path.
-	head := Clean(elem[0])
+	head := clean(elem[0], windows)
 	if isUNC(head) {
 		return p
 	}
 	// head + tail == UNC, but joining two non-UNC paths should not result
 	// in a UNC path. Undo creation of UNC path.
-	tail := Clean(strings.Join(elem[1:], string(Separator)))
-	if head[len(head)-1] == Separator {
+	tail := clean(strings.Join(elem[1:], string(windowsSeparator)), windows)
+	if head[len(head)-1] == windowsSeparator {
 		return head + tail
 	}
-	return head + string(Separator) + tail
+	return head + string(windowsSeparator) + tail
 }
 
 // isUNC reports whether path is a UNC path.
 func isUNC(path string) bool {
-	return volumeNameLen(path) > 2
+	return windows.volumeNameLen(path) > 2
 }
 
-func sameWord(a, b string) bool {
+func (o windowsInfo) sameWord(a, b string) bool {
 	return strings.EqualFold(a, b)
 }
diff --git a/pkg/path/testdata/path_windows_test.go b/pkg/path/testdata/path_windows_test.go
index 9309a7d..28d1ebb 100644
--- a/pkg/path/testdata/path_windows_test.go
+++ b/pkg/path/testdata/path_windows_test.go
@@ -1,26 +1,36 @@
+// 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 2013 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_test
+package path
 
 import (
 	"flag"
 	"fmt"
-	"internal/testenv"
-	"io/fs"
 	"io/ioutil"
-	"os"
+	goos "os"
 	"os/exec"
-	"path/filepath"
 	"reflect"
-	"runtime/debug"
 	"strings"
 	"testing"
 )
 
 func TestWinSplitListTestsAreValid(t *testing.T) {
-	comspec := os.Getenv("ComSpec")
+	comspec := goos.Getenv("ComSpec")
 	if comspec == "" {
 		t.Fatal("%ComSpec% must be set")
 	}
@@ -34,35 +44,35 @@
 	comspec string) {
 
 	const (
-		cmdfile             = `printdir.cmd`
-		perm    fs.FileMode = 0700
+		cmdfile = `printdir.cmd`
+		perm    = 0700
 	)
 
 	tmp, err := ioutil.TempDir("", "testWinSplitListTestIsValid")
 	if err != nil {
 		t.Fatalf("TempDir failed: %v", err)
 	}
-	defer os.RemoveAll(tmp)
+	defer goos.RemoveAll(tmp)
 
 	for i, d := range tt.result {
 		if d == "" {
 			continue
 		}
-		if cd := filepath.Clean(d); filepath.VolumeName(cd) != "" ||
+		if cd := Clean(d, Windows); VolumeName(cd, Windows) != "" ||
 			cd[0] == '\\' || cd == ".." || (len(cd) >= 3 && cd[0:3] == `..\`) {
 			t.Errorf("%d,%d: %#q refers outside working directory", ti, i, d)
 			return
 		}
-		dd := filepath.Join(tmp, d)
-		if _, err := os.Stat(dd); err == nil {
+		dd := Join([]string{tmp, d}, Windows)
+		if _, err := goos.Stat(dd); err == nil {
 			t.Errorf("%d,%d: %#q already exists", ti, i, d)
 			return
 		}
-		if err = os.MkdirAll(dd, perm); err != nil {
+		if err = goos.MkdirAll(dd, perm); err != nil {
 			t.Errorf("%d,%d: MkdirAll(%#q) failed: %v", ti, i, dd, err)
 			return
 		}
-		fn, data := filepath.Join(dd, cmdfile), []byte("@echo "+d+"\r\n")
+		fn, data := Join([]string{dd, cmdfile}, Windows), []byte("@echo "+d+"\r\n")
 		if err = ioutil.WriteFile(fn, data, perm); err != nil {
 			t.Errorf("%d,%d: WriteFile(%#q) failed: %v", ti, i, fn, err)
 			return
@@ -70,7 +80,7 @@
 	}
 
 	// on some systems, SystemRoot is required for cmd to work
-	systemRoot := os.Getenv("SystemRoot")
+	systemRoot := goos.Getenv("SystemRoot")
 
 	for i, d := range tt.result {
 		if d == "" {
@@ -93,7 +103,7 @@
 			return
 		default:
 			// unshadow cmdfile in next directory
-			err = os.Remove(filepath.Join(tmp, d, cmdfile))
+			err = goos.Remove(Join([]string{tmp, d, cmdfile}, Windows))
 			if err != nil {
 				t.Fatalf("Remove test command failed: %v", err)
 			}
@@ -101,126 +111,6 @@
 	}
 }
 
-func TestWindowsEvalSymlinks(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-
-	tmpDir, err := ioutil.TempDir("", "TestWindowsEvalSymlinks")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	// /tmp may itself be a symlink! Avoid the confusion, although
-	// it means trusting the thing we're testing.
-	tmpDir, err = filepath.EvalSymlinks(tmpDir)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if len(tmpDir) < 3 {
-		t.Fatalf("tmpDir path %q is too short", tmpDir)
-	}
-	if tmpDir[1] != ':' {
-		t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir)
-	}
-	test := EvalSymlinksTest{"test/linkabswin", tmpDir[:3]}
-
-	// Create the symlink farm using relative paths.
-	testdirs := append(EvalSymlinksTestDirs, test)
-	for _, d := range testdirs {
-		var err error
-		path := simpleJoin(tmpDir, d.path)
-		if d.dest == "" {
-			err = os.Mkdir(path, 0755)
-		} else {
-			err = os.Symlink(d.dest, path)
-		}
-		if err != nil {
-			t.Fatal(err)
-		}
-	}
-
-	path := simpleJoin(tmpDir, test.path)
-
-	testEvalSymlinks(t, path, test.dest)
-
-	testEvalSymlinksAfterChdir(t, path, ".", test.dest)
-
-	testEvalSymlinksAfterChdir(t,
-		path,
-		filepath.VolumeName(tmpDir)+".",
-		test.dest)
-
-	testEvalSymlinksAfterChdir(t,
-		simpleJoin(tmpDir, "test"),
-		simpleJoin("..", test.path),
-		test.dest)
-
-	testEvalSymlinksAfterChdir(t, tmpDir, test.path, test.dest)
-}
-
-// TestEvalSymlinksCanonicalNames verify that EvalSymlinks
-// returns "canonical" path names on windows.
-func TestEvalSymlinksCanonicalNames(t *testing.T) {
-	tmp, err := ioutil.TempDir("", "evalsymlinkcanonical")
-	if err != nil {
-		t.Fatal("creating temp dir:", err)
-	}
-	defer os.RemoveAll(tmp)
-
-	// ioutil.TempDir might return "non-canonical" name.
-	cTmpName, err := filepath.EvalSymlinks(tmp)
-	if err != nil {
-		t.Errorf("EvalSymlinks(%q) error: %v", tmp, err)
-	}
-
-	dirs := []string{
-		"test",
-		"test/dir",
-		"testing_long_dir",
-		"TEST2",
-	}
-
-	for _, d := range dirs {
-		dir := filepath.Join(cTmpName, d)
-		err := os.Mkdir(dir, 0755)
-		if err != nil {
-			t.Fatal(err)
-		}
-		cname, err := filepath.EvalSymlinks(dir)
-		if err != nil {
-			t.Errorf("EvalSymlinks(%q) error: %v", dir, err)
-			continue
-		}
-		if dir != cname {
-			t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", dir, cname, dir)
-			continue
-		}
-		// test non-canonical names
-		test := strings.ToUpper(dir)
-		p, err := filepath.EvalSymlinks(test)
-		if err != nil {
-			t.Errorf("EvalSymlinks(%q) error: %v", test, err)
-			continue
-		}
-		if p != cname {
-			t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname)
-			continue
-		}
-		// another test
-		test = strings.ToLower(dir)
-		p, err = filepath.EvalSymlinks(test)
-		if err != nil {
-			t.Errorf("EvalSymlinks(%q) error: %v", test, err)
-			continue
-		}
-		if p != cname {
-			t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname)
-			continue
-		}
-	}
-}
-
 // checkVolume8dot3Setting runs "fsutil 8dot3name query c:" command
 // (where c: is vol parameter) to discover "8dot3 name creation state".
 // The state is combination of 2 flags. The global flag controls if it
@@ -287,301 +177,3 @@
 }
 
 var runFSModifyTests = flag.Bool("run_fs_modify_tests", false, "run tests which modify filesystem parameters")
-
-// This test assumes registry state of NtfsDisable8dot3NameCreation is 2,
-// the default (Volume level setting).
-func TestEvalSymlinksCanonicalNamesWith8dot3Disabled(t *testing.T) {
-	if !*runFSModifyTests {
-		t.Skip("skipping test that modifies file system setting; enable with -run_fs_modify_tests")
-	}
-	tempVol := filepath.VolumeName(os.TempDir())
-	if len(tempVol) != 2 {
-		t.Fatalf("unexpected temp volume name %q", tempVol)
-	}
-
-	err := checkVolume8dot3Setting(tempVol, true)
-	if err != nil {
-		t.Fatal(err)
-	}
-	err = setVolume8dot3Setting(tempVol, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		err := setVolume8dot3Setting(tempVol, true)
-		if err != nil {
-			t.Fatal(err)
-		}
-		err = checkVolume8dot3Setting(tempVol, true)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}()
-	err = checkVolume8dot3Setting(tempVol, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-	TestEvalSymlinksCanonicalNames(t)
-}
-
-func TestToNorm(t *testing.T) {
-	stubBase := func(path string) (string, error) {
-		vol := filepath.VolumeName(path)
-		path = path[len(vol):]
-
-		if strings.Contains(path, "/") {
-			return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
-		}
-
-		if path == "" || path == "." || path == `\` {
-			return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
-		}
-
-		i := strings.LastIndexByte(path, filepath.Separator)
-		if i == len(path)-1 { // trailing '\' is invalid
-			return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
-		}
-		if i == -1 {
-			return strings.ToUpper(path), nil
-		}
-
-		return strings.ToUpper(path[i+1:]), nil
-	}
-
-	// On this test, toNorm should be same as string.ToUpper(filepath.Clean(path)) except empty string.
-	tests := []struct {
-		arg  string
-		want string
-	}{
-		{"", ""},
-		{".", "."},
-		{"./foo/bar", `FOO\BAR`},
-		{"/", `\`},
-		{"/foo/bar", `\FOO\BAR`},
-		{"/foo/bar/baz/qux", `\FOO\BAR\BAZ\QUX`},
-		{"foo/bar", `FOO\BAR`},
-		{"C:/foo/bar", `C:\FOO\BAR`},
-		{"C:foo/bar", `C:FOO\BAR`},
-		{"c:/foo/bar", `C:\FOO\BAR`},
-		{"C:/foo/bar", `C:\FOO\BAR`},
-		{"C:/foo/bar/", `C:\FOO\BAR`},
-		{`C:\foo\bar`, `C:\FOO\BAR`},
-		{`C:\foo/bar\`, `C:\FOO\BAR`},
-		{"C:/ふー/バー", `C:\ふー\バー`},
-	}
-
-	for _, test := range tests {
-		got, err := filepath.ToNorm(test.arg, stubBase)
-		if err != nil {
-			t.Errorf("toNorm(%s) failed: %v\n", test.arg, err)
-		} else if got != test.want {
-			t.Errorf("toNorm(%s) returns %s, but %s expected\n", test.arg, got, test.want)
-		}
-	}
-
-	testPath := `{{tmp}}\test\foo\bar`
-
-	testsDir := []struct {
-		wd   string
-		arg  string
-		want string
-	}{
-		// test absolute paths
-		{".", `{{tmp}}\test\foo\bar`, `{{tmp}}\test\foo\bar`},
-		{".", `{{tmp}}\.\test/foo\bar`, `{{tmp}}\test\foo\bar`},
-		{".", `{{tmp}}\test\..\test\foo\bar`, `{{tmp}}\test\foo\bar`},
-		{".", `{{tmp}}\TEST\FOO\BAR`, `{{tmp}}\test\foo\bar`},
-
-		// test relative paths begin with drive letter
-		{`{{tmp}}\test`, `{{tmpvol}}.`, `{{tmpvol}}.`},
-		{`{{tmp}}\test`, `{{tmpvol}}..`, `{{tmpvol}}..`},
-		{`{{tmp}}\test`, `{{tmpvol}}foo\bar`, `{{tmpvol}}foo\bar`},
-		{`{{tmp}}\test`, `{{tmpvol}}.\foo\bar`, `{{tmpvol}}foo\bar`},
-		{`{{tmp}}\test`, `{{tmpvol}}foo\..\foo\bar`, `{{tmpvol}}foo\bar`},
-		{`{{tmp}}\test`, `{{tmpvol}}FOO\BAR`, `{{tmpvol}}foo\bar`},
-
-		// test relative paths begin with '\'
-		{"{{tmp}}", `{{tmpnovol}}\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
-		{"{{tmp}}", `{{tmpnovol}}\.\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
-		{"{{tmp}}", `{{tmpnovol}}\test\..\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
-		{"{{tmp}}", `{{tmpnovol}}\TEST\FOO\BAR`, `{{tmpnovol}}\test\foo\bar`},
-
-		// test relative paths begin without '\'
-		{`{{tmp}}\test`, ".", `.`},
-		{`{{tmp}}\test`, "..", `..`},
-		{`{{tmp}}\test`, `foo\bar`, `foo\bar`},
-		{`{{tmp}}\test`, `.\foo\bar`, `foo\bar`},
-		{`{{tmp}}\test`, `foo\..\foo\bar`, `foo\bar`},
-		{`{{tmp}}\test`, `FOO\BAR`, `foo\bar`},
-
-		// test UNC paths
-		{".", `\\localhost\c$`, `\\localhost\c$`},
-	}
-
-	tmp, err := ioutil.TempDir("", "testToNorm")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		err := os.RemoveAll(tmp)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}()
-
-	// ioutil.TempDir might return "non-canonical" name.
-	ctmp, err := filepath.EvalSymlinks(tmp)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = os.MkdirAll(strings.ReplaceAll(testPath, "{{tmp}}", ctmp), 0777)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	cwd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		err := os.Chdir(cwd)
-		if err != nil {
-			t.Fatal(err)
-		}
-	}()
-
-	tmpVol := filepath.VolumeName(ctmp)
-	if len(tmpVol) != 2 {
-		t.Fatalf("unexpected temp volume name %q", tmpVol)
-	}
-
-	tmpNoVol := ctmp[len(tmpVol):]
-
-	replacer := strings.NewReplacer("{{tmp}}", ctmp, "{{tmpvol}}", tmpVol, "{{tmpnovol}}", tmpNoVol)
-
-	for _, test := range testsDir {
-		wd := replacer.Replace(test.wd)
-		arg := replacer.Replace(test.arg)
-		want := replacer.Replace(test.want)
-
-		if test.wd == "." {
-			err := os.Chdir(cwd)
-			if err != nil {
-				t.Error(err)
-
-				continue
-			}
-		} else {
-			err := os.Chdir(wd)
-			if err != nil {
-				t.Error(err)
-
-				continue
-			}
-		}
-
-		got, err := filepath.ToNorm(arg, filepath.NormBase)
-		if err != nil {
-			t.Errorf("toNorm(%s) failed: %v (wd=%s)\n", arg, err, wd)
-		} else if got != want {
-			t.Errorf("toNorm(%s) returns %s, but %s expected (wd=%s)\n", arg, got, want, wd)
-		}
-	}
-}
-
-func TestUNC(t *testing.T) {
-	// Test that this doesn't go into an infinite recursion.
-	// See golang.org/issue/15879.
-	defer debug.SetMaxStack(debug.SetMaxStack(1e6))
-	filepath.Glob(`\\?\c:\*`)
-}
-
-func testWalkMklink(t *testing.T, linktype string) {
-	output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
-	if !strings.Contains(string(output), fmt.Sprintf(" /%s ", linktype)) {
-		t.Skipf(`skipping test; mklink does not supports /%s parameter`, linktype)
-	}
-	testWalkSymlink(t, func(target, link string) error {
-		output, err := exec.Command("cmd", "/c", "mklink", "/"+linktype, link, target).CombinedOutput()
-		if err != nil {
-			return fmt.Errorf(`"mklink /%s %v %v" command failed: %v\n%v`, linktype, link, target, err, string(output))
-		}
-		return nil
-	})
-}
-
-func TestWalkDirectoryJunction(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-	testWalkMklink(t, "J")
-}
-
-func TestWalkDirectorySymlink(t *testing.T) {
-	testenv.MustHaveSymlink(t)
-	testWalkMklink(t, "D")
-}
-
-func TestNTNamespaceSymlink(t *testing.T) {
-	output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
-	if !strings.Contains(string(output), " /J ") {
-		t.Skip("skipping test because mklink command does not support junctions")
-	}
-
-	tmpdir, err := ioutil.TempDir("", "TestNTNamespaceSymlink")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmpdir)
-
-	// Make sure tmpdir is not a symlink, otherwise tests will fail.
-	tmpdir, err = filepath.EvalSymlinks(tmpdir)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	vol := filepath.VolumeName(tmpdir)
-	output, err = exec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput()
-	if err != nil {
-		t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output)
-	}
-	target := strings.Trim(string(output), " \n\r")
-
-	dirlink := filepath.Join(tmpdir, "dirlink")
-	output, err = exec.Command("cmd", "/c", "mklink", "/J", dirlink, target).CombinedOutput()
-	if err != nil {
-		t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, target, err, output)
-	}
-
-	got, err := filepath.EvalSymlinks(dirlink)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if want := vol + `\`; got != want {
-		t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, want)
-	}
-
-	// Make sure we have sufficient privilege to run mklink command.
-	testenv.MustHaveSymlink(t)
-
-	file := filepath.Join(tmpdir, "file")
-	err = ioutil.WriteFile(file, []byte(""), 0666)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	target += file[len(filepath.VolumeName(file)):]
-
-	filelink := filepath.Join(tmpdir, "filelink")
-	output, err = exec.Command("cmd", "/c", "mklink", filelink, target).CombinedOutput()
-	if err != nil {
-		t.Fatalf("failed to run mklink %v %v: %v %q", filelink, target, err, output)
-	}
-
-	got, err = filepath.EvalSymlinks(filelink)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if want := file; got != want {
-		t.Errorf(`EvalSymlinks(%q): got %q, want %q`, filelink, got, want)
-	}
-}
diff --git a/pkg/path/testdata/pathtxtar_test.go b/pkg/path/testdata/pathtxtar_test.go
new file mode 100644
index 0000000..af861d9
--- /dev/null
+++ b/pkg/path/testdata/pathtxtar_test.go
@@ -0,0 +1,25 @@
+// 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_test
+
+import (
+	"testing"
+
+	"cuelang.org/go/pkg/internal/builtintest"
+)
+
+func TestBuiltin(t *testing.T) {
+	builtintest.Run("path", t)
+}
diff --git a/pkg/path/testdata/symlink.go b/pkg/path/testdata/symlink.go
deleted file mode 100644
index 6fefd15..0000000
--- a/pkg/path/testdata/symlink.go
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright 2012 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
-
-import (
-	"errors"
-	"io/fs"
-	"os"
-	"runtime"
-	"syscall"
-)
-
-func walkSymlinks(path string) (string, error) {
-	volLen := volumeNameLen(path)
-	pathSeparator := string(os.PathSeparator)
-
-	if volLen < len(path) && os.IsPathSeparator(path[volLen]) {
-		volLen++
-	}
-	vol := path[:volLen]
-	dest := vol
-	linksWalked := 0
-	for start, end := volLen, volLen; start < len(path); start = end {
-		for start < len(path) && os.IsPathSeparator(path[start]) {
-			start++
-		}
-		end = start
-		for end < len(path) && !os.IsPathSeparator(path[end]) {
-			end++
-		}
-
-		// On Windows, "." can be a symlink.
-		// We look it up, and use the value if it is absolute.
-		// If not, we just return ".".
-		isWindowsDot := runtime.GOOS == "windows" && path[volumeNameLen(path):] == "."
-
-		// The next path component is in path[start:end].
-		if end == start {
-			// No more path components.
-			break
-		} else if path[start:end] == "." && !isWindowsDot {
-			// Ignore path component ".".
-			continue
-		} else if path[start:end] == ".." {
-			// Back up to previous component if possible.
-			// Note that volLen includes any leading slash.
-
-			// Set r to the index of the last slash in dest,
-			// after the volume.
-			var r int
-			for r = len(dest) - 1; r >= volLen; r-- {
-				if os.IsPathSeparator(dest[r]) {
-					break
-				}
-			}
-			if r < volLen || dest[r+1:] == ".." {
-				// Either path has no slashes
-				// (it's empty or just "C:")
-				// or it ends in a ".." we had to keep.
-				// Either way, keep this "..".
-				if len(dest) > volLen {
-					dest += pathSeparator
-				}
-				dest += ".."
-			} else {
-				// Discard everything since the last slash.
-				dest = dest[:r]
-			}
-			continue
-		}
-
-		// Ordinary path component. Add it to result.
-
-		if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
-			dest += pathSeparator
-		}
-
-		dest += path[start:end]
-
-		// Resolve symlink.
-
-		fi, err := os.Lstat(dest)
-		if err != nil {
-			return "", err
-		}
-
-		if fi.Mode()&fs.ModeSymlink == 0 {
-			if !fi.Mode().IsDir() && end < len(path) {
-				return "", syscall.ENOTDIR
-			}
-			continue
-		}
-
-		// Found symlink.
-
-		linksWalked++
-		if linksWalked > 255 {
-			return "", errors.New("EvalSymlinks: too many links")
-		}
-
-		link, err := os.Readlink(dest)
-		if err != nil {
-			return "", err
-		}
-
-		if isWindowsDot && !IsAbs(link) {
-			// On Windows, if "." is a relative symlink,
-			// just return ".".
-			break
-		}
-
-		path = link + path[end:]
-
-		v := volumeNameLen(link)
-		if v > 0 {
-			// Symlink to drive name is an absolute path.
-			if v < len(link) && os.IsPathSeparator(link[v]) {
-				v++
-			}
-			vol = link[:v]
-			dest = vol
-			end = len(vol)
-		} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
-			// Symlink to absolute path.
-			dest = link[:1]
-			end = 1
-		} else {
-			// Symlink to relative path; replace last
-			// path component in dest.
-			var r int
-			for r = len(dest) - 1; r >= volLen; r-- {
-				if os.IsPathSeparator(dest[r]) {
-					break
-				}
-			}
-			if r < volLen {
-				dest = vol
-			} else {
-				dest = dest[:r]
-			}
-			end = 0
-		}
-	}
-	return Clean(dest), nil
-}
diff --git a/pkg/path/testdata/symlink_unix.go b/pkg/path/testdata/symlink_unix.go
deleted file mode 100644
index d20e63a..0000000
--- a/pkg/path/testdata/symlink_unix.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// +build !windows
-
-package filepath
-
-func evalSymlinks(path string) (string, error) {
-	return walkSymlinks(path)
-}
diff --git a/pkg/path/testdata/symlink_windows.go b/pkg/path/testdata/symlink_windows.go
deleted file mode 100644
index d72279e..0000000
--- a/pkg/path/testdata/symlink_windows.go
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2012 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
-
-import (
-	"strings"
-	"syscall"
-)
-
-// normVolumeName is like VolumeName, but makes drive letter upper case.
-// result of EvalSymlinks must be unique, so we have
-// EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`).
-func normVolumeName(path string) string {
-	volume := VolumeName(path)
-
-	if len(volume) > 2 { // isUNC
-		return volume
-	}
-
-	return strings.ToUpper(volume)
-}
-
-// normBase returns the last element of path with correct case.
-func normBase(path string) (string, error) {
-	p, err := syscall.UTF16PtrFromString(path)
-	if err != nil {
-		return "", err
-	}
-
-	var data syscall.Win32finddata
-
-	h, err := syscall.FindFirstFile(p, &data)
-	if err != nil {
-		return "", err
-	}
-	syscall.FindClose(h)
-
-	return syscall.UTF16ToString(data.FileName[:]), nil
-}
-
-// baseIsDotDot reports whether the last element of path is "..".
-// The given path should be 'Clean'-ed in advance.
-func baseIsDotDot(path string) bool {
-	i := strings.LastIndexByte(path, Separator)
-	return path[i+1:] == ".."
-}
-
-// toNorm returns the normalized path that is guaranteed to be unique.
-// It should accept the following formats:
-//   * UNC paths                              (e.g \\server\share\foo\bar)
-//   * absolute paths                         (e.g C:\foo\bar)
-//   * relative paths begin with drive letter (e.g C:foo\bar, C:..\foo\bar, C:.., C:.)
-//   * relative paths begin with '\'          (e.g \foo\bar)
-//   * relative paths begin without '\'       (e.g foo\bar, ..\foo\bar, .., .)
-// The returned normalized path will be in the same form (of 5 listed above) as the input path.
-// If two paths A and B are indicating the same file with the same format, toNorm(A) should be equal to toNorm(B).
-// The normBase parameter should be equal to the normBase func, except for in tests.  See docs on the normBase func.
-func toNorm(path string, normBase func(string) (string, error)) (string, error) {
-	if path == "" {
-		return path, nil
-	}
-
-	path = Clean(path)
-
-	volume := normVolumeName(path)
-	path = path[len(volume):]
-
-	// skip special cases
-	if path == "" || path == "." || path == `\` {
-		return volume + path, nil
-	}
-
-	var normPath string
-
-	for {
-		if baseIsDotDot(path) {
-			normPath = path + `\` + normPath
-
-			break
-		}
-
-		name, err := normBase(volume + path)
-		if err != nil {
-			return "", err
-		}
-
-		normPath = name + `\` + normPath
-
-		i := strings.LastIndexByte(path, Separator)
-		if i == -1 {
-			break
-		}
-		if i == 0 { // `\Go` or `C:\Go`
-			normPath = `\` + normPath
-
-			break
-		}
-
-		path = path[:i]
-	}
-
-	normPath = normPath[:len(normPath)-1] // remove trailing '\'
-
-	return volume + normPath, nil
-}
-
-func evalSymlinks(path string) (string, error) {
-	newpath, err := walkSymlinks(path)
-	if err != nil {
-		return "", err
-	}
-	newpath, err = toNorm(newpath, normBase)
-	if err != nil {
-		return "", err
-	}
-	return newpath, nil
-}