cue/errors: add more Position information

- adds Position convenience funtion
- key position is now sorted first
- add same unwrapping support Path'

Issue #52

Change-Id: I883e810de0131a7e10a467e0d53b0c513dbba351
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2213
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/errors.go b/cue/errors.go
index 0d415e8..8fce493 100644
--- a/cue/errors.go
+++ b/cue/errors.go
@@ -44,6 +44,8 @@
 	return e.n.Pos()
 }
 
+func (e *nodeError) InputPositions() []token.Pos { return nil }
+
 func (e *nodeError) Path() []string {
 	return e.path
 }
@@ -71,7 +73,7 @@
 	return e.err.Pos()
 }
 
-func (e *valueError) Positions() []token.Pos {
+func (e *valueError) InputPositions() []token.Pos {
 	return e.err.Positions()
 }
 
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index e60e8c4..f512028 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -63,8 +63,15 @@
 
 // Error is the common error message.
 type Error interface {
+	// Position returns the primary position of an error. If multiple positions
+	// contribute equally, this reflects one of them.
 	Position() token.Pos
 
+	// InputPositions reports positions that contributed to an error, including
+	// the expressions resulting in the conflict, as well as values that were
+	// the input to this expression.
+	InputPositions() []token.Pos
+
 	// Error reports the error message without position information.
 	Error() string
 
@@ -77,16 +84,49 @@
 	Msg() (format string, args []interface{})
 }
 
-// Path returns the path of an Error if err is of that type.
-func Path(err error) []string {
-	e, ok := err.(Error)
-	if !ok {
+// Positions returns all positions returned by an error, sorted
+// by relevance when possible and with duplicates removed.
+func Positions(err error) []token.Pos {
+	e := Error(nil)
+	if !xerrors.As(err, &e) {
 		return nil
 	}
-	return e.Path()
+
+	a := make([]token.Pos, 0, 3)
+
+	sortOffset := 0
+	pos := e.Position()
+	if pos.IsValid() {
+		a = append(a, pos)
+		sortOffset = 1
+	}
+
+	for _, p := range e.InputPositions() {
+		if p.IsValid() && p != pos {
+			a = append(a, p)
+		}
+	}
+
+	byPos := byPos(a[sortOffset:])
+	sort.Sort(byPos)
+	k := unique.ToFront(byPos)
+	return a[:k+sortOffset]
 }
 
-// // TODO: make Error an interface that returns a list of positions.
+type byPos []token.Pos
+
+func (s *byPos) Truncate(n int)    { (*s) = (*s)[:n] }
+func (s byPos) Len() int           { return len(s) }
+func (s byPos) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+func (s byPos) Less(i, j int) bool { return comparePos(s[i], s[j]) == -1 }
+
+// Path returns the path of an Error if err is of that type.
+func Path(err error) []string {
+	if e := Error(nil); xerrors.As(err, &e) {
+		return e.Path()
+	}
+	return nil
+}
 
 // Newf creates an Error with the associated position and message.
 func Newf(p token.Pos, format string, args ...interface{}) Error {
@@ -124,6 +164,8 @@
 	return Path(p.err)
 }
 
+func (p *posError) InputPositions() []token.Pos { return nil }
+
 // E creates a new error.
 func E(args ...interface{}) error {
 	e := &posError{}
@@ -237,26 +279,39 @@
 func (p List) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
 
 func (p List) Less(i, j int) bool {
-	e := p[i].Position()
-	f := p[j].Position()
+	if c := comparePos(p[i].Position(), p[j].Position()); c != 0 {
+		return c == -1
+	}
 	// Note that it is not sufficient to simply compare file offsets because
 	// the offsets do not reflect modified line information (through //line
 	// comments).
-	if e.Filename() != f.Filename() {
-		return e.Filename() < f.Filename()
-	}
-	if e.Line() != f.Line() {
-		return e.Line() < f.Line()
-	}
-	if e.Column() != f.Column() {
-		return e.Column() < f.Column()
-	}
+
 	if !equalPath(p[i].Path(), p[j].Path()) {
 		return lessPath(p[i].Path(), p[j].Path())
 	}
 	return p[i].Error() < p[j].Error()
 }
 
+func lessOrMore(isLess bool) int {
+	if isLess {
+		return -1
+	}
+	return 1
+}
+
+func comparePos(a, b token.Pos) int {
+	if a.Filename() != b.Filename() {
+		return lessOrMore(a.Filename() < b.Filename())
+	}
+	if a.Line() != b.Line() {
+		return lessOrMore(a.Line() < b.Line())
+	}
+	if a.Column() != b.Column() {
+		return lessOrMore(a.Column() < b.Column())
+	}
+	return 0
+}
+
 func lessPath(a, b []string) bool {
 	for i, x := range a {
 		if i >= len(b) {
@@ -366,21 +421,8 @@
 	}
 
 	positions := []string{}
-
-	for e := err; e != nil; e = xerrors.Unwrap(e) {
-		switch x := e.(type) {
-		case interface{ Positions() []token.Pos }:
-			for _, p := range x.Positions() {
-				if p.IsValid() {
-					positions = append(positions, p.String())
-				}
-			}
-
-		case interface{ Position() token.Pos }:
-			if pos := x.Position().String(); pos != "-" {
-				positions = append(positions, pos)
-			}
-		}
+	for _, p := range Positions(err) {
+		positions = append(positions, p.String())
 	}
 
 	if path := Path(err); path != nil {
@@ -392,8 +434,6 @@
 		return
 	}
 
-	unique.Strings(&positions)
-
 	fprintf(w, "%v:\n", err)
 	for _, pos := range positions {
 		fprintf(w, "    %s\n", pos)
diff --git a/cue/types.go b/cue/types.go
index 8ab4d11..c05ca61 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -167,8 +167,11 @@
 }
 
 func (e *marshalError) Path() []string               { return e.err.Path() }
-func (e *marshalError) Position() token.Pos          { return e.err.Position() }
 func (e *marshalError) Msg() (string, []interface{}) { return e.err.Msg() }
+func (e *marshalError) Position() token.Pos          { return e.err.Position() }
+func (e *marshalError) InputPositions() []token.Pos {
+	return e.err.InputPositions()
+}
 
 func unwrapJSONError(err error) errors.Error {
 	switch x := err.(type) {