cue: allow hidden fields in Path

This requires users to explicitly specify the package in which
such hidden identifiers should be scoped.

Note:
1) Hid is not called Hidden because this was already
taken. Maybe in a big API rework some renaming would be
handy.

2) We do not allow "" to mean the anonymous packages,
as this will allow using this to mean the "top-level" package
in the future, which will be the common case.

3) When converting from AST identifiers, we could see if it
is bound to a certain package already and then use that
scope. This does not conflict with the current API.

Change-Id: If3438b84b2f6032072342abe58cde1e1bafdc4f3
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9185
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/build/instance.go b/cue/build/instance.go
index 90eb64d..bf9dc77 100644
--- a/cue/build/instance.go
+++ b/cue/build/instance.go
@@ -131,7 +131,10 @@
 // ID returns the package ID unique for this module.
 func (inst *Instance) ID() string {
 	if inst.PkgName == "" {
-		return ""
+		return "_"
+	}
+	if s := inst.ImportPath; s != "" {
+		return s
 	}
 	s := fmt.Sprintf("%s:%s", inst.Module, inst.PkgName)
 	return s
diff --git a/cue/examples_test.go b/cue/examples_test.go
new file mode 100644
index 0000000..330234d
--- /dev/null
+++ b/cue/examples_test.go
@@ -0,0 +1,67 @@
+// Copyright 2021 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 cue_test
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/internal/cuetxtar"
+	"github.com/rogpeppe/go-internal/txtar"
+)
+
+func load(file string) *cue.Instance {
+	dir, _ := ioutil.TempDir("", "*")
+	defer os.RemoveAll(dir)
+
+	inst := cue.Build(cuetxtar.Load(txtar.Parse([]byte(file)), dir))[0]
+	if err := inst.Err; err != nil {
+		panic(err)
+	}
+	return inst
+}
+
+func ExampleHid() {
+	const file = `
+-- cue.mod/module.cue --
+module: "acme.com"
+
+-- main.cue --
+import "acme.com/foo:bar"
+
+bar
+_foo: int // scoped in main (anonymous) package
+baz: _foo
+
+-- foo/bar.cue --
+package bar
+
+_foo: int // scoped within imported package
+bar: _foo
+`
+
+	v := load(file).Value()
+
+	v = v.FillPath(cue.MakePath(cue.Hid("_foo", "acme.com/foo:bar")), 1)
+	v = v.FillPath(cue.MakePath(cue.Hid("_foo", "_")), 2)
+	fmt.Println(v.LookupPath(cue.ParsePath("bar")).Int64())
+	fmt.Println(v.LookupPath(cue.ParsePath("baz")).Int64())
+
+	// Output:
+	// 1 <nil>
+	// 2 <nil>
+}
diff --git a/cue/path.go b/cue/path.go
index 497094b..21f48f0 100644
--- a/cue/path.go
+++ b/cue/path.go
@@ -15,6 +15,7 @@
 package cue
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
 
@@ -130,19 +131,37 @@
 		} else {
 			sel = basicLitSelector(b)
 		}
-		return append(a, sel)
+		return appendSelector(a, sel)
 
 	case *ast.SelectorExpr:
 		a := toSelectors(x.X)
-		return append(a, identSelector(x.Sel))
+		return appendSelector(a, identSelector(x.Sel))
 
 	default:
-		return []Selector{Selector{pathError{
+		return []Selector{{pathError{
 			errors.Newf(token.NoPos, "invalid label %s ", internal.DebugStr(x)),
 		}}}
 	}
 }
 
+// appendSelector is like append(a, sel), except that it collects errors
+// in a one-element slice.
+func appendSelector(a []Selector, sel Selector) []Selector {
+	err, isErr := sel.sel.(pathError)
+	if len(a) == 1 {
+		if p, ok := a[0].sel.(pathError); ok {
+			if isErr {
+				p.Error = errors.Append(p.Error, err.Error)
+			}
+			return a
+		}
+	}
+	if isErr {
+		return []Selector{sel}
+	}
+	return append(a, sel)
+}
+
 func basicLitSelector(b *ast.BasicLit) Selector {
 	switch b.Kind {
 	case token.INT:
@@ -181,10 +200,17 @@
 func identSelector(label ast.Label) Selector {
 	switch x := label.(type) {
 	case *ast.Ident:
-		if isHiddenOrDefinition(x.Name) {
+		switch s := x.Name; {
+		case strings.HasPrefix(s, "_"):
+			// TODO: extract package from a bound identifier.
+			return Selector{pathError{errors.Newf(token.NoPos,
+				"invalid path: hidden label %s not allowed", s),
+			}}
+		case strings.HasPrefix(s, "#"):
 			return Selector{definitionSelector(x.Name)}
+		default:
+			return Selector{stringSelector(x.Name)}
 		}
-		return Selector{stringSelector(x.Name)}
 
 	case *ast.BasicLit:
 		return basicLitSelector(x)
@@ -211,13 +237,58 @@
 	return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_")
 }
 
+// Hid returns a selector for a hidden field. It panics is pkg is empty.
+// Hidden fields are scoped by package, and pkg indicates for which package
+// the hidden field must apply.For anonymous packages, it must be set to "_".
+func Hid(name, pkg string) Selector {
+	if !ast.IsValidIdent(name) {
+		panic(fmt.Sprintf("invalid identifier %s", name))
+	}
+	if !strings.HasPrefix(name, "_") {
+		panic(fmt.Sprintf("%s is not a hidden field identifier", name))
+	}
+	if pkg == "" {
+		panic(fmt.Sprintf("missing package for hidden identifier %s", name))
+	}
+	return Selector{scopedSelector{name, pkg}}
+}
+
+type scopedSelector struct {
+	name, pkg string
+}
+
+// String returns the CUE representation of the definition.
+func (s scopedSelector) String() string {
+	return s.name
+}
+
+func (s scopedSelector) kind() adt.FeatureType {
+	switch {
+	case strings.HasPrefix(s.name, "#"):
+		return adt.DefinitionLabel
+	case strings.HasPrefix(s.name, "_#"):
+		return adt.HiddenDefinitionLabel
+	case strings.HasPrefix(s.name, "_"):
+		return adt.HiddenLabel
+	default:
+		return adt.StringLabel
+	}
+}
+
+func (s scopedSelector) feature(r adt.Runtime) adt.Feature {
+	return adt.MakeIdentLabel(r, s.name, s.pkg)
+}
+
 // A Def marks a string as a definition label. An # will be added if a string is
-// not prefixed with an # or _# already. Hidden labels are qualified by the
-// package in which they are looked up.
+// not prefixed with a #. It will panic if s cannot be written as a valid
+// identifier.
 func Def(s string) Selector {
-	if !isHiddenOrDefinition(s) {
+	if !strings.HasPrefix(s, "#") {
 		s = "#" + s
 	}
+	if !ast.IsValidIdent(s) {
+		panic(fmt.Sprintf("invalid definition %s", s))
+	}
 	return Selector{definitionSelector(s)}
 }
 
@@ -229,16 +300,7 @@
 }
 
 func (d definitionSelector) kind() adt.FeatureType {
-	switch {
-	case strings.HasPrefix(string(d), "#"):
-		return adt.DefinitionLabel
-	case strings.HasPrefix(string(d), "_#"):
-		return adt.HiddenDefinitionLabel
-	case strings.HasPrefix(string(d), "_"):
-		return adt.HiddenLabel
-	default:
-		panic("invalid definition")
-	}
+	return adt.DefinitionLabel
 }
 
 func (d definitionSelector) feature(r adt.Runtime) adt.Feature {
diff --git a/cue/path_test.go b/cue/path_test.go
index 935698c..388df11 100644
--- a/cue/path_test.go
+++ b/cue/path_test.go
@@ -20,14 +20,11 @@
 )
 
 func Test(t *testing.T) {
-	p := func(a ...Selector) Path {
-		return Path{path: a}
-	}
-	_ = p
 	var r Runtime
 	inst, _ := r.Compile("", `
 		#Foo:   a: b: 1
 		"#Foo": c: d: 2
+		_foo: b: 5
 		a: 3
 		b: [4, 5, 6]
 		c: "#Foo": 7
@@ -38,7 +35,7 @@
 		str  string
 		err  bool
 	}{{
-		path: p(Def("#Foo"), Str("a"), Str("b")),
+		path: MakePath(Def("#Foo"), Str("a"), Str("b")),
 		out:  "1",
 		str:  "#Foo.a.b",
 	}, {
@@ -51,18 +48,22 @@
 		str:  `"#Foo".c.d`,
 	}, {
 		// fallback Def(Foo) -> Def(#Foo)
-		path: p(Def("Foo"), Str("a"), Str("b")),
+		path: MakePath(Def("Foo"), Str("a"), Str("b")),
 		out:  "1",
 		str:  "#Foo.a.b",
 	}, {
-		path: p(Str("b"), Index(2)),
+		path: MakePath(Str("b"), Index(2)),
 		out:  "6",
 		str:  "b[2]", // #Foo.b.2
 	}, {
-		path: p(Str("c"), Str("#Foo")),
+		path: MakePath(Str("c"), Str("#Foo")),
 		out:  "7",
 		str:  `c."#Foo"`,
 	}, {
+		path: MakePath(Hid("_foo", "_"), Str("b")),
+		out:  "5",
+		str:  `_foo.b`,
+	}, {
 		path: ParsePath("#Foo.a.b"),
 		str:  "#Foo.a.b",
 		out:  "1",
@@ -82,7 +83,7 @@
 		path: ParsePath("foo._foo"),
 		str:  "_|_",
 		err:  true,
-		out:  `_|_ // invalid path: hidden fields not allowed in path foo._foo`,
+		out:  `_|_ // invalid path: hidden label _foo not allowed`,
 	}, {
 		path: ParsePath(`c."#Foo`),
 		str:  "_|_",
diff --git a/cue/testdata/definitions/hidden.txtar b/cue/testdata/definitions/hidden.txtar
index cd25867..ae398ac 100644
--- a/cue/testdata/definitions/hidden.txtar
+++ b/cue/testdata/definitions/hidden.txtar
@@ -49,7 +49,7 @@
     }
   }
   d: (#struct){
-    _val(example.com:pkg): (#struct){
+    _val(example.com/pkg): (#struct){
       f: (int){ 3 }
     }
     _name(:foo): (struct){
@@ -61,7 +61,7 @@
   }
   e: (_|_){
     // [eval]
-    _val(example.com:pkg): (#struct){
+    _val(example.com/pkg): (#struct){
       f: (int){ 3 }
     }
     _name(:foo): (_|_){
@@ -79,7 +79,7 @@
     }
   }
   f: (#struct){
-    _val(example.com:pkg): (#struct){
+    _val(example.com/pkg): (#struct){
       f: (int){ 3 }
     }
     _val(:foo): (struct){
diff --git a/cue/types_test.go b/cue/types_test.go
index de7e1ad..088e171 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1039,6 +1039,19 @@
 		`,
 	}, {
 		in: `
+		foo: _foo: int
+		bar: foo._foo
+		`,
+		x:    3,
+		path: MakePath(Str("foo"), Hid("_foo", "_")),
+		out: `
+		foo: {
+			_foo: 3
+		}
+		bar: 3
+		`,
+	}, {
+		in: `
 		string
 		`,
 		x:    "foo",
diff --git a/internal/core/compile/compile.go b/internal/core/compile/compile.go
index f94b18f..14ea284 100644
--- a/internal/core/compile/compile.go
+++ b/internal/core/compile/compile.go
@@ -42,7 +42,7 @@
 	Imports func(x *ast.Ident) (pkgPath string)
 
 	// pkgPath is used to qualify the scope of hidden fields. The default
-	// scope is "main".
+	// scope is "_".
 	pkgPath string
 }
 
@@ -85,7 +85,7 @@
 		c.Config = *cfg
 	}
 	if pkgPath == "" {
-		pkgPath = "main"
+		pkgPath = "_"
 	}
 	c.Config.pkgPath = pkgPath
 	return c
diff --git a/internal/core/compile/compile_test.go b/internal/core/compile/compile_test.go
index 1bea647..3ec921d 100644
--- a/internal/core/compile/compile_test.go
+++ b/internal/core/compile/compile_test.go
@@ -53,7 +53,7 @@
 
 		a := t.ValidInstances()
 
-		v, err := compile.Files(nil, r, "main", a[0].Files...)
+		v, err := compile.Files(nil, r, "", a[0].Files...)
 
 		// Write the results.
 		t.WriteErrors(err)
@@ -98,7 +98,7 @@
 	}
 	r := runtime.New()
 
-	arc, err := compile.Files(nil, r, "main", file)
+	arc, err := compile.Files(nil, r, "", file)
 	if err != nil {
 		t.Error(errors.Details(err, nil))
 	}
diff --git a/internal/core/debug/debug.go b/internal/core/debug/debug.go
index 6bcd680..125028c 100644
--- a/internal/core/debug/debug.go
+++ b/internal/core/debug/debug.go
@@ -92,7 +92,7 @@
 func (w *printer) labelString(f adt.Feature) string {
 	if f.IsHidden() {
 		ident := f.IdentString(w.index)
-		if pkgName := f.PkgID(w.index); pkgName != "main" {
+		if pkgName := f.PkgID(w.index); pkgName != "_" {
 			ident = fmt.Sprintf("%s(%s)", ident, pkgName)
 		}
 		return ident