cue: handle builtins used as values

The API needs to handle the cases of builtins
being able to be used as validator without a call.

They are a bit weird in that they are concrete,
but not ground, thus the explicit distinction is
introduced.

Change-Id: I11202c797ea3bddbb322d8c370c8ccbe38e5199e
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2721
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin.go b/cue/builtin.go
index 21167d5..2b2672a 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -173,6 +173,13 @@
 	},
 }
 
+func (x *builtin) representedKind() kind {
+	if x.isValidator() {
+		return x.Params[0]
+	}
+	return x.kind()
+}
+
 func (x *builtin) kind() kind {
 	return lambdaKind
 }
@@ -195,9 +202,13 @@
 	return fmt.Sprintf("%s.%s", ctx.labelStr(x.pkg), x.Name)
 }
 
+func (x *builtin) isValidator() bool {
+	return len(x.Params) == 1 && x.Result == boolKind
+}
+
 func convertBuiltin(v evaluated) evaluated {
 	x, ok := v.(*builtin)
-	if ok && len(x.Params) == 1 && x.Result == boolKind {
+	if ok && x.isValidator() {
 		return &customValidator{v.base(), []evaluated{}, x}
 	}
 	return v
diff --git a/cue/eval.go b/cue/eval.go
index 74256db..1aa390f 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -56,7 +56,11 @@
 	v := e.eval(x.x, structKind|lambdaKind, msgType, x)
 
 	if e.is(v, structKind|lambdaKind, "") {
-		n := v.(scope).lookup(ctx, x.feature)
+		sc, ok := v.(scope)
+		if !ok {
+			return ctx.mkErr(x, "invalid subject to selector (found %v)", v.kind())
+		}
+		n := sc.lookup(ctx, x.feature)
 		if n.val() == nil {
 			field := ctx.labelStr(x.feature)
 			//	m.foo undefined (type map[string]bool has no field or method foo)
diff --git a/cue/export_test.go b/cue/export_test.go
index ef18af0..4aa01e7 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -446,6 +446,20 @@
 				b: int
 			})
 		}`),
+	}, {
+		in: `
+		import "time"
+
+		a: { b: time.Duration } | { c: time.Duration }
+		`,
+		out: unindent(`
+		import "time"
+
+		a: {
+			b: time.Duration
+		} | {
+			c: time.Duration
+		}`),
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
diff --git a/cue/kind.go b/cue/kind.go
index 425df87..9deac1b 100644
--- a/cue/kind.go
+++ b/cue/kind.go
@@ -88,7 +88,8 @@
 // isDone means that the value will not evaluate further.
 func (k kind) isDone() bool        { return k&referenceKind == bottomKind }
 func (k kind) hasReferences() bool { return k&referenceKind != bottomKind }
-func (k kind) isGround() bool      { return k&nonGround == bottomKind }
+func (k kind) isConcrete() bool    { return k&^(lambdaKind-1) == bottomKind }
+func (k kind) isGround() bool      { return k&^(nonGround-1) == bottomKind }
 func (k kind) isAtom() bool        { return k.isGround() && k&atomKind != bottomKind }
 func (k kind) isAnyOf(of kind) bool {
 	return k&of != bottomKind
diff --git a/cue/types.go b/cue/types.go
index bb40acf..dc1fcb1 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -647,7 +647,16 @@
 	if v.path == nil {
 		return BottomKind
 	}
-	k := v.path.v.evalPartial(v.ctx()).kind()
+	var k kind
+	x := v.path.v.evalPartial(v.ctx())
+	switch x := convertBuiltin(x).(type) {
+	case *builtin:
+		k = x.representedKind()
+	case *customValidator:
+		k = x.call.Params[0]
+	default:
+		k = x.kind()
+	}
 	vk := BottomKind // Everything is a bottom kind.
 	for i := kind(1); i < nonGround; i <<= 1 {
 		if k&i != 0 {
@@ -824,8 +833,8 @@
 	return pos
 }
 
-// IsConcrete reports whether the current value is a concrete scalar value,
-// not relying on default values, a terminal error, a list, or a struct.
+// IsConcrete reports whether the current value is a concrete scalar value
+// (not relying on default values), a terminal error, a list, or a struct.
 // It does not verify that values of lists or structs are concrete themselves.
 // To check whether there is a concrete default, use v.Default().IsConcrete().
 func (v Value) IsConcrete() bool {
@@ -839,17 +848,17 @@
 		return false
 	}
 	// Other errors are considered ground.
-	return x.kind().isGround()
+	return x.kind().isConcrete()
 }
 
-// IsIncomplete is deprecated.
+// Deprecated: IsIncomplete
 //
 // It indicates that the value cannot be fully evaluated due to
 // insufficient information.
 func (v Value) IsIncomplete() bool {
 	// TODO: remove
 	x := v.eval(v.ctx())
-	if x.kind().hasReferences() || !x.kind().isGround() {
+	if !x.kind().isConcrete() {
 		return true
 	}
 	return isIncomplete(x)
diff --git a/cue/types_test.go b/cue/types_test.go
index df59170..163e5a4 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -158,6 +158,30 @@
 		value:    `{a: int, b: [1][a]}.b`,
 		kind:     BottomKind,
 		concrete: false,
+	}, {
+		value: `import "time"
+			{a: time.Time}.a`,
+		kind:           BottomKind,
+		incompleteKind: StringKind,
+		concrete:       false,
+	}, {
+		value: `import "time"
+			{a: time.Time & string}.a`,
+		kind:           BottomKind,
+		incompleteKind: StringKind,
+		concrete:       false,
+	}, {
+		value: `import "strings"
+			{a: strings.ContainsAny("D")}.a`,
+		kind:           BottomKind,
+		incompleteKind: StringKind,
+		concrete:       false,
+	}, {
+		value: `import "struct"
+		{a: struct.MaxFields(2) & {}}.a`,
+		kind:           StructKind, // Can determine a valid struct already.
+		incompleteKind: StructKind,
+		concrete:       true,
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.value, func(t *testing.T) {
@@ -1109,6 +1133,13 @@
 			`,
 		opts: []Option{DisallowCycles(true)},
 		err:  true,
+	}, {
+		desc: "builtins are okay",
+		in: `
+		import "time"
+
+		a: { b: time.Duration } | { c: time.Duration }
+		`,
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {