cue: improve marshaling error message

And show a useful position at which the
marshaling fails. Adding file locations may
even be better, but will be a bit more involved.

Change-Id: If047ab6b55f4c062af8d27b016feaa69de65709b
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2002
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/export.go b/cmd/cue/cmd/export.go
index 5f651d0..ae6fb8e 100644
--- a/cmd/cue/cmd/export.go
+++ b/cmd/cue/cmd/export.go
@@ -83,6 +83,9 @@
 		root := instances[0].Value()
 		err := e.Encode(root)
 		if err != nil {
+			if x, ok := err.(*json.MarshalerError); ok {
+				err = x.Err
+			}
 			fmt.Fprintln(w, err)
 		}
 		return nil
diff --git a/cmd/cue/cmd/export_test.go b/cmd/cue/cmd/export_test.go
index 4431cb1..77f7f83 100644
--- a/cmd/cue/cmd/export_test.go
+++ b/cmd/cue/cmd/export_test.go
@@ -18,4 +18,5 @@
 
 func TestExport(t *testing.T) {
 	runCommand(t, exportCmd.RunE, "export")
+	runCommand(t, exportCmd.RunE, "export_err")
 }
diff --git a/cmd/cue/cmd/testdata/exporterr/export_err.cue b/cmd/cue/cmd/testdata/exporterr/export_err.cue
new file mode 100644
index 0000000..480a942
--- /dev/null
+++ b/cmd/cue/cmd/testdata/exporterr/export_err.cue
@@ -0,0 +1,5 @@
+package exporterr
+
+a: {
+	b: [0, 1, {c: int}, 3]
+}
diff --git a/cmd/cue/cmd/testdata/exporterr/export_err.out b/cmd/cue/cmd/testdata/exporterr/export_err.out
new file mode 100644
index 0000000..8ce55eb
--- /dev/null
+++ b/cmd/cue/cmd/testdata/exporterr/export_err.out
@@ -0,0 +1 @@
+cue: marshal error at path a.b.2.c: cannot convert incomplete value "int" to JSON
diff --git a/cue/types.go b/cue/types.go
index b3d0bcc..90b9510 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -112,20 +112,20 @@
 
 // MarshalJSON returns a valid JSON encoding or reports an error if any of the
 // fields is invalid.
-func (o *structValue) MarshalJSON() (b []byte, err error) {
+func (o *structValue) marshalJSON() (b []byte, err error) {
 	b = append(b, '{')
 	n := o.Len()
 	for i := 0; i < n; i++ {
 		k, v := o.At(i)
 		s, err := json.Marshal(k)
 		if err != nil {
-			return nil, err
+			return nil, unwrapJSONError(k, err)
 		}
 		b = append(b, s...)
 		b = append(b, ':')
 		bb, err := json.Marshal(v)
 		if err != nil {
-			return nil, err
+			return nil, unwrapJSONError(k, err)
 		}
 		b = append(b, bb...)
 		if i < n-1 {
@@ -136,6 +136,34 @@
 	return b, nil
 }
 
+type marshalError struct {
+	path string
+	err  error
+	// TODO: maybe also collect the file locations contributing to the
+	// failing values?
+}
+
+func (e *marshalError) Error() string {
+	return fmt.Sprintf("cue: marshal error at path %s: %v", e.path, e.err)
+}
+
+func unwrapJSONError(key interface{}, err error) error {
+	if je, ok := err.(*json.MarshalerError); ok {
+		err = je.Err
+	}
+	path := make([]string, 0, 2)
+	if key != nil {
+		path = append(path, fmt.Sprintf("%v", key))
+	}
+	if me, ok := err.(*marshalError); ok {
+		if me.path != "" {
+			path = append(path, me.path)
+		}
+		err = me.err
+	}
+	return &marshalError{strings.Join(path, "."), err}
+}
+
 // An Iterator iterates over values.
 //
 type Iterator struct {
@@ -193,10 +221,10 @@
 func marshalList(l *Iterator) (b []byte, err error) {
 	b = append(b, '[')
 	if l.Next() {
-		for {
+		for i := 0; ; i++ {
 			x, err := json.Marshal(l.Value())
 			if err != nil {
-				return nil, err
+				return nil, unwrapJSONError(i, err)
 			}
 			b = append(b, x...)
 			if !l.Next() {
@@ -583,6 +611,14 @@
 
 // MarshalJSON marshalls this value into valid JSON.
 func (v Value) MarshalJSON() (b []byte, err error) {
+	b, err = v.marshalJSON()
+	if err != nil {
+		return nil, unwrapJSONError(nil, err)
+	}
+	return b, nil
+}
+
+func (v Value) marshalJSON() (b []byte, err error) {
 	v, _ = v.Default()
 	if v.path == nil {
 		return json.Marshal(nil)
@@ -607,7 +643,7 @@
 		return marshalList(&i)
 	case structKind:
 		obj, _ := v.structVal(ctx)
-		return obj.MarshalJSON()
+		return obj.marshalJSON()
 	case bottomKind:
 		return nil, x.(*bottom)
 	default: