cue: add appendPath for creating paths

Part of larger error improvements.

Issue #52

Change-Id: I287b69f05350929a5655727a8015bc994a7fddb4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2201
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/types.go b/cue/types.go
index 75637da..810753c 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -23,6 +23,7 @@
 	"math/big"
 	"strconv"
 	"strings"
+	"unicode"
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
@@ -474,6 +475,47 @@
 	arc
 }
 
+// path returns the path of the value.
+func (v *valueData) appendPath(a []string, idx *index) ([]string, kind) {
+	var k kind
+	if v.parent != nil {
+		a, k = v.parent.appendPath(a, idx)
+	}
+	switch k {
+	case listKind:
+		a = append(a, strconv.FormatInt(int64(v.index), 10))
+	case structKind:
+		f := idx.labelStr(v.arc.feature)
+		if !isIdent(f) && !isNumber(f) {
+			f = quote(f, '"')
+		}
+		a = append(a, f)
+	}
+	return a, v.arc.cache.kind()
+}
+
+var validIdent = []*unicode.RangeTable{unicode.L, unicode.N}
+
+func isIdent(s string) bool {
+	valid := []*unicode.RangeTable{unicode.Letter}
+	for _, r := range s {
+		if !unicode.In(r, valid...) && r != '_' {
+			return false
+		}
+		valid = validIdent
+	}
+	return true
+}
+
+func isNumber(s string) bool {
+	for _, r := range s {
+		if r < '0' || '9' < r {
+			return false
+		}
+	}
+	return true
+}
+
 // Value holds any value, which may be a Boolean, Error, List, Null, Number,
 // Struct, or String.
 type Value struct {
diff --git a/cue/types_test.go b/cue/types_test.go
index 4df1cc3..5e6ed27 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -21,6 +21,7 @@
 	"math"
 	"math/big"
 	"reflect"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -1054,6 +1055,65 @@
 	}
 }
 
+func TestPath(t *testing.T) {
+	config := `
+	a b c: 5
+	b: {
+		b1: 3
+		b2: 4
+		"b 3": 5
+		"4b": 6
+		l: [
+			{a: 2},
+			{c: 2},
+		]
+	}
+	`
+	mkpath := func(p ...string) []string { return p }
+	testCases := [][]string{
+		mkpath("a", "b", "c"),
+		mkpath("b", "l", "1", "c"),
+		mkpath("b", `"b 3"`),
+		mkpath("b", `"4b"`),
+	}
+	for _, tc := range testCases {
+		r := Runtime{}
+		inst, err := r.Parse("config", config)
+		if err != nil {
+			t.Fatal(err)
+		}
+		t.Run(strings.Join(tc, "."), func(t *testing.T) {
+			v := inst.Lookup(tc[0])
+			for _, e := range tc[1:] {
+				if '0' <= e[0] && e[0] <= '9' {
+					i, err := strconv.Atoi(e)
+					if err != nil {
+						t.Fatal(err)
+					}
+					iter, err := v.List()
+					if err != nil {
+						t.Fatal(err)
+					}
+					for c := 0; iter.Next(); c++ {
+						if c == i {
+							v = iter.Value()
+							break
+						}
+					}
+				} else if e[0] == '"' {
+					v = v.Lookup(e[1 : len(e)-1])
+				} else {
+					v = v.Lookup(e)
+				}
+			}
+			got, _ := v.path.appendPath(nil, v.idx)
+			if !reflect.DeepEqual(got, tc) {
+				t.Errorf("got %v; want %v", got, tc)
+			}
+		})
+	}
+}
+
 func TestValueLookup(t *testing.T) {
 	config := `
 		a: {