cue: add Kind.String method

Change-Id: If7b149cebf514c22b7c446ef85999adea200a4e0
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3741
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/types.go b/cue/types.go
index e22a9f1..4fe7942 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -22,6 +22,7 @@
 	"io"
 	"math"
 	"math/big"
+	"math/bits"
 	"strconv"
 	"strings"
 	"unicode"
@@ -66,10 +67,68 @@
 
 	nextKind
 
+	// _numberKind is used as a implementation detail inside
+	// Kind.String to indicate NumberKind.
+	_numberKind
+
 	// NumberKind represents any kind of number.
 	NumberKind = IntKind | FloatKind
 )
 
+// String returns the representation of the Kind as
+// a CUE expression. For example:
+//
+//	(IntKind|ListKind).String()
+//
+// will return:
+//
+//	(int|[...])
+func (k Kind) String() string {
+	if k == BottomKind {
+		return "_|_"
+	}
+	if (k & NumberKind) == NumberKind {
+		k = (k &^ NumberKind) | _numberKind
+	}
+	var buf strings.Builder
+	multiple := bits.OnesCount(uint(k)) > 1
+	if multiple {
+		buf.WriteByte('(')
+	}
+	for count := 0; ; count++ {
+		n := bits.TrailingZeros(uint(k))
+		if n == bits.UintSize {
+			break
+		}
+		bit := Kind(1 << uint(n))
+		k &^= bit
+		s, ok := kindStrs[bit]
+		if !ok {
+			s = fmt.Sprintf("bad(%d)", n)
+		}
+		if count > 0 {
+			buf.WriteByte('|')
+		}
+		buf.WriteString(s)
+	}
+	if multiple {
+		buf.WriteByte(')')
+	}
+	return buf.String()
+}
+
+var kindStrs = map[Kind]string{
+	NullKind:    "null",
+	BoolKind:    "bool",
+	IntKind:     "int",
+	FloatKind:   "float",
+	StringKind:  "string",
+	BytesKind:   "bytes",
+	StructKind:  "{...}",
+	ListKind:    "[...]",
+	_numberKind: "number",
+}
+
 // An structValue represents a JSON object.
 //
 // TODO: remove
diff --git a/cue/types_test.go b/cue/types_test.go
index d225375..cc991f1 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -764,7 +764,7 @@
 		// 	length: "2",
 	}, {
 		input:  "3",
-		length: "_|_(len not supported for type 4)", // TODO: fix kind name
+		length: "_|_(len not supported for type int)",
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.input, func(t *testing.T) {
@@ -2187,3 +2187,54 @@
 		})
 	}
 }
+
+func TestKindString(t *testing.T) {
+	testCases := []struct {
+		input Kind
+		want  string
+	}{{
+		input: BottomKind,
+		want:  "_|_",
+	}, {
+		input: IntKind | ListKind,
+		want:  `(int|[...])`,
+	}, {
+		input: NullKind,
+		want:  "null",
+	}, {
+		input: IntKind,
+		want:  "int",
+	}, {
+		input: FloatKind,
+		want:  "float",
+	}, {
+		input: StringKind,
+		want:  "string",
+	}, {
+		input: BytesKind,
+		want:  "bytes",
+	}, {
+		input: StructKind,
+		want:  "{...}",
+	}, {
+		input: ListKind,
+		want:  "[...]",
+	}, {
+		input: NumberKind,
+		want:  "number",
+	}, {
+		input: BoolKind | NumberKind | ListKind,
+		want:  "(bool|[...]|number)",
+	}, {
+		input: 1 << 20,
+		want:  "bad(20)",
+	}}
+	for _, tc := range testCases {
+		t.Run(tc.want, func(t *testing.T) {
+			got := tc.input.String()
+			if got != tc.want {
+				t.Errorf("\n got %v;\nwant %v", got, tc.want)
+			}
+		})
+	}
+}