From dcb2a1fb4fd53cd55c0d9d6e7ae1502fd0f6522e Mon Sep 17 00:00:00 2001
From: Marcel van Lohuizen <mpvl@golang.org>
Date: Tue, 20 Apr 2021 12:01:36 +0200
Subject: [PATCH] cue/errors: add Wrap

introduces new internal wrapping error that allows
harmonizing wrapped errors.

Issue #52

Change-Id: I93844d4cc7592521b70b6402a9670a941c2f6dfc
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9447
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
---
 cmd/cue/cmd/testdata/script/cmd_err.txt       |  1 +
 cmd/cue/cmd/testdata/script/cmd_errpos.txt    |  1 +
 cmd/cue/cmd/testdata/script/export_err.txt    |  1 +
 cmd/cue/cmd/testdata/script/vet_yaml.txt      |  1 +
 cue/errors/errors.go                          | 96 +++++++++++++------
 cue/testdata/eval/dynamic_field.txtar         |  4 +
 cue/testdata/fulleval/055_issue318.txtar      |  4 +
 .../interpolation/041_interpolation.txtar     |  3 +
 cue/testdata/interpolation/incomplete.txtar   |  1 +
 cue/testdata/references/errors.txtar          |  2 +
 internal/core/adt/errors.go                   |  5 -
 internal/core/adt/expr.go                     |  3 +-
 internal/task/task.go                         |  6 +-
 pkg/encoding/json/testdata/gen.txtar          |  4 +-
 pkg/encoding/yaml/testdata/gen.txtar          |  8 +-
 pkg/list/testdata/gen.txtar                   | 19 +++-
 pkg/list/testdata/list.txtar                  |  3 +-
 pkg/math/testdata/gen.txtar                   |  3 +-
 18 files changed, 113 insertions(+), 52 deletions(-)

diff --git a/cmd/cue/cmd/testdata/script/cmd_err.txt b/cmd/cue/cmd/testdata/script/cmd_err.txt
index a3b31d868..3319baa7c 100644
--- a/cmd/cue/cmd/testdata/script/cmd_err.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_err.txt
@@ -10,6 +10,7 @@ command.ref.task.display.filename: invalid string argument: non-concrete value s
     ./task_tool.cue:6:8
     ./task_tool.cue:7:9
     tool/file:15:3
+    tool/file:15:16
 -- task_tool.cue --
 package home
 
diff --git a/cmd/cue/cmd/testdata/script/cmd_errpos.txt b/cmd/cue/cmd/testdata/script/cmd_errpos.txt
index 328fcac10..f2db94818 100644
--- a/cmd/cue/cmd/testdata/script/cmd_errpos.txt
+++ b/cmd/cue/cmd/testdata/script/cmd_errpos.txt
@@ -12,6 +12,7 @@ command.prompter.contents: invalid bytes argument: non-concrete value string:
 command.prompter.filename: invalid string argument: non-concrete value string:
     ./task_tool.cue:9:10
     tool/file:9:3
+    tool/file:9:16
 -- task_tool.cue --
 package foo
 
diff --git a/cmd/cue/cmd/testdata/script/export_err.txt b/cmd/cue/cmd/testdata/script/export_err.txt
index 6e6a41595..ae52f07b7 100644
--- a/cmd/cue/cmd/testdata/script/export_err.txt
+++ b/cmd/cue/cmd/testdata/script/export_err.txt
@@ -13,6 +13,7 @@ cmp stderr expect-stderr
 a.b.2.c: incomplete value int
 out: invalid interpolation: undefined field d:
     ./exporterr/export_err.cue:7:6
+    ./exporterr/export_err.cue:7:16
 -- expect-stdout --
 -- exporterr/export_err.cue --
 package exporterr
diff --git a/cmd/cue/cmd/testdata/script/vet_yaml.txt b/cmd/cue/cmd/testdata/script/vet_yaml.txt
index e61ed735c..0370ffdd6 100644
--- a/cmd/cue/cmd/testdata/script/vet_yaml.txt
+++ b/cmd/cue/cmd/testdata/script/vet_yaml.txt
@@ -4,6 +4,7 @@ cmp stderr expect-stderr
 -- expect-stderr --
 phrases: invalid value "phrases:\n  # A quote from Mark Twain.\n  quote1:\n    lang: en\n    attribution: Mark Twain\n\n  # A Norwegian proverb.\n  proverb:\n    lang: no\n    text: Stemmen som sier at du ikke klarer det, lyver." (does not satisfy encoding/yaml.Validate({phrases:{},#Phrase:{lang:=~"^[a-zA-Z0-9-_]{2,}$" | false,text:!=""},#LanguageTag:=~"^[a-zA-Z0-9-_]{2,}$" | false})): error in call to encoding/yaml.Validate: incomplete value !="":
     ./yaml.cue:19:10
+    ./yaml.cue:11:17
     ./yaml.cue:21:10
 -- yaml.cue --
 import "encoding/yaml"
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index 20b5bd0fd..e8e5a0ec7 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -170,25 +170,77 @@ func Newf(p token.Pos, format string, args ...interface{}) Error {
 // Wrapf creates an Error with the associated position and message. The provided
 // error is added for inspection context.
 func Wrapf(err error, p token.Pos, format string, args ...interface{}) Error {
-	a, ok := err.(list)
+	pErr := &posError{
+		pos:     p,
+		Message: NewMessage(format, args),
+	}
+	return Wrap(pErr, err)
+}
+
+// Wrap creates a new error where child is a subordinate error of parent.
+// If child is list of Errors, the result will itself be a list of errors
+// where child is a subordinate error of each parent.
+func Wrap(parent Error, child error) Error {
+	if child == nil {
+		return parent
+	}
+	a, ok := child.(list)
 	if !ok {
-		return &posError{
-			pos:     p,
-			Message: NewMessage(format, args),
-			err:     err,
-		}
+		return &wrapped{parent, child}
 	}
-	b := make([]Error, len(a))
+	b := make(list, len(a))
 	for i, err := range a {
-		b[i] = &posError{
-			pos:     p,
-			Message: NewMessage(format, args),
-			err:     err,
-		}
+		b[i] = &wrapped{parent, err}
+	}
+	return b
+}
+
+type wrapped struct {
+	main Error
+	wrap error
+}
+
+// Error implements the error interface.
+func (e *wrapped) Error() string {
+	switch msg := e.main.Error(); {
+	case e.wrap == nil:
+		return msg
+	case msg == "":
+		return e.wrap.Error()
+	default:
+		return fmt.Sprintf("%s: %s", msg, e.wrap)
+	}
+}
+
+func (e *wrapped) Msg() (format string, args []interface{}) {
+	return e.main.Msg()
+}
+
+func (e *wrapped) Path() []string {
+	if p := Path(e.main); p != nil {
+		return p
+	}
+	return Path(e.wrap)
+}
+
+func (e *wrapped) InputPositions() []token.Pos {
+	return append(e.main.InputPositions(), Positions(e.wrap)...)
+}
+
+func (e *wrapped) Position() token.Pos {
+	if p := e.main.Position(); p != token.NoPos {
+		return p
+	}
+	if wrap, ok := e.wrap.(Error); ok {
+		return wrap.Position()
 	}
-	return list(b)
+	return token.NoPos
 }
 
+func (e *wrapped) Unwrap() error { return e.wrap }
+
+func (e *wrapped) Cause() error { return e.wrap }
+
 // Promote converts a regular Go error to an Error if it isn't already one.
 func Promote(err error, msg string) Error {
 	switch x := err.(type) {
@@ -209,27 +261,11 @@ type posError struct {
 	pos    token.Pos
 	inputs []token.Pos
 	Message
-
-	// The underlying error that triggered this one, if any.
-	err error
 }
 
-func (e *posError) Path() []string              { return Path(e.err) }
+func (e *posError) Path() []string              { return nil }
 func (e *posError) InputPositions() []token.Pos { return e.inputs }
 func (e *posError) Position() token.Pos         { return e.pos }
-func (e *posError) Unwrap() error               { return e.err }
-func (e *posError) Cause() error                { return e.err }
-
-// Error implements the error interface.
-func (e *posError) Error() string {
-	if e.err == nil {
-		return e.Message.Error()
-	}
-	if e.Message.format == "" {
-		return e.err.Error()
-	}
-	return fmt.Sprintf("%s: %s", e.Message.Error(), e.err)
-}
 
 // Append combines two errors, flattening Lists as necessary.
 func Append(a, b Error) Error {
diff --git a/cue/testdata/eval/dynamic_field.txtar b/cue/testdata/eval/dynamic_field.txtar
index 003ee5ecc..b35259381 100644
--- a/cue/testdata/eval/dynamic_field.txtar
+++ b/cue/testdata/eval/dynamic_field.txtar
@@ -25,6 +25,8 @@ parenExprRefEqual: {
 Errors:
 invalid interpolation: conflicting values 2 and 1:
     ./in.cue:12:31
+    ./in.cue:12:34
+    ./in.cue:12:36
 
 Result:
 (_|_){
@@ -40,6 +42,8 @@ Result:
     issue799: (_|_){
       // [eval] invalid interpolation: conflicting values 2 and 1:
       //     ./in.cue:12:31
+      //     ./in.cue:12:34
+      //     ./in.cue:12:36
       key: (int){ &(>=-2147483648, <=2147483647, int) }
     }
   }
diff --git a/cue/testdata/fulleval/055_issue318.txtar b/cue/testdata/fulleval/055_issue318.txtar
index b43df7119..7b1383221 100644
--- a/cue/testdata/fulleval/055_issue318.txtar
+++ b/cue/testdata/fulleval/055_issue318.txtar
@@ -45,8 +45,10 @@
 Errors:
 #T.out1: invalid interpolation: undefined field y:
     ./in.cue:3:8
+    ./in.cue:3:24
 #T.out2: invalid interpolation: undefined field y:
     ./in.cue:4:8
+    ./in.cue:4:15
 #T.vy: undefined field y:
     ./in.cue:6:12
 
@@ -61,10 +63,12 @@ Result:
     out1: (_|_){
       // [eval] #T.out1: invalid interpolation: undefined field y:
       //     ./in.cue:3:8
+      //     ./in.cue:3:24
     }
     out2: (_|_){
       // [eval] #T.out2: invalid interpolation: undefined field y:
       //     ./in.cue:4:8
+      //     ./in.cue:4:15
     }
     vx: (string){ string }
     vy: (_|_){
diff --git a/cue/testdata/interpolation/041_interpolation.txtar b/cue/testdata/interpolation/041_interpolation.txtar
index 8f2dd9bd7..ea18b8e32 100644
--- a/cue/testdata/interpolation/041_interpolation.txtar
+++ b/cue/testdata/interpolation/041_interpolation.txtar
@@ -35,6 +35,7 @@ e: _|_ // expression in interpolation must evaluate to a number kind or string (
 Errors:
 e: invalid interpolation: cannot use [] (type list) as type (bool|string|bytes|number):
     ./in.cue:7:4
+    ./in.cue:7:7
 
 Result:
 (_|_){
@@ -49,10 +50,12 @@ Result:
   u: (_|_){
     // [incomplete] u: invalid interpolation: non-concrete value _ (type _):
     //     ./in.cue:5:4
+    //     ./in.cue:5:7
   }
   r: (_){ _ }
   e: (_|_){
     // [eval] e: invalid interpolation: cannot use [] (type list) as type (bool|string|bytes|number):
     //     ./in.cue:7:4
+    //     ./in.cue:7:7
   }
 }
diff --git a/cue/testdata/interpolation/incomplete.txtar b/cue/testdata/interpolation/incomplete.txtar
index 29a5343ad..8d5290822 100644
--- a/cue/testdata/interpolation/incomplete.txtar
+++ b/cue/testdata/interpolation/incomplete.txtar
@@ -27,6 +27,7 @@ out: """
   out: (_|_){
     // [incomplete] out: invalid interpolation: undefined field #d:
     //     ./in.cue:8:6
+    //     ./in.cue:13:21
   }
 }
 -- out/compile --
diff --git a/cue/testdata/references/errors.txtar b/cue/testdata/references/errors.txtar
index e33c5360a..4e33acf8c 100644
--- a/cue/testdata/references/errors.txtar
+++ b/cue/testdata/references/errors.txtar
@@ -80,10 +80,12 @@ Result:
     r1: (_|_){
       // [incomplete] missingFieldNestedInInterpolation.r1: invalid interpolation: undefined field b:
       //     ./references.cue:27:9
+      //     ./references.cue:27:14
     }
     r2: (_|_){
       // [incomplete] missingFieldNestedInInterpolation.r2: invalid interpolation: undefined field d:
       //     ./references.cue:30:9
+      //     ./references.cue:30:14
     }
   }
 }
diff --git a/internal/core/adt/errors.go b/internal/core/adt/errors.go
index c990092bd..20cfc6ddb 100644
--- a/internal/core/adt/errors.go
+++ b/internal/core/adt/errors.go
@@ -211,7 +211,6 @@ type ValueError struct {
 	v      *Vertex
 	pos    token.Pos
 	auxpos []token.Pos
-	err    errors.Error
 	errors.Message
 }
 
@@ -325,7 +324,3 @@ func (e *ValueError) Path() (a []string) {
 	}
 	return a
 }
-
-func (e ValueError) Unwrap() error {
-	return e.err
-}
diff --git a/internal/core/adt/expr.go b/internal/core/adt/expr.go
index 2e44ce883..714b8157e 100644
--- a/internal/core/adt/expr.go
+++ b/internal/core/adt/expr.go
@@ -1484,13 +1484,12 @@ func validateWithBuiltin(c *OpContext, src token.Pos, b *Builtin, args []Value)
 	}
 
 	vErr := c.NewPosf(src, "invalid value %s (does not satisfy %s)", args[0], buf.String())
-	vErr.err = err
 
 	for _, v := range args {
 		vErr.AddPosition(v)
 	}
 
-	return &Bottom{Code: severeness, Err: vErr}
+	return &Bottom{Code: severeness, Err: errors.Wrap(vErr, err)}
 }
 
 // A Disjunction represents a disjunction, where each disjunct may or may not
diff --git a/internal/task/task.go b/internal/task/task.go
index 1b43bb4fb..e8e585fd0 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -88,9 +88,8 @@ func (c *Context) addErr(v cue.Value, wrap error, format string, args ...interfa
 		task:    c.Obj,
 		v:       v,
 		Message: errors.NewMessage(format, args),
-		err:     wrap,
 	}
-	c.Err = errors.Append(c.Err, err)
+	c.Err = errors.Append(c.Err, errors.Wrap(err, wrap))
 }
 
 // taskError wraps some error values to retain position information about the
@@ -99,7 +98,6 @@ type taskError struct {
 	task cue.Value
 	v    cue.Value
 	errors.Message
-	err error
 }
 
 var _ errors.Error = &taskError{}
@@ -126,8 +124,6 @@ func (t *taskError) InputPositions() (a []token.Pos) {
 	return a
 }
 
-func (t *taskError) Unwrap() error { return t.err }
-
 // A RunnerFunc creates a Runner.
 type RunnerFunc func(v cue.Value) (Runner, error)
 
diff --git a/pkg/encoding/json/testdata/gen.txtar b/pkg/encoding/json/testdata/gen.txtar
index f59b37927..f54b90098 100644
--- a/pkg/encoding/json/testdata/gen.txtar
+++ b/pkg/encoding/json/testdata/gen.txtar
@@ -17,7 +17,9 @@ t8: {
 t9: json.MarshalStream([{a: 1}, {b: int | *2}])
 -- out/json --
 Errors:
-a: error in call to encoding/json.Validate: invalid value 10 (out of bound <3)
+a: error in call to encoding/json.Validate: invalid value 10 (out of bound <3):
+    ./in.cue:4:36
+    json.Validate:1:6
 
 Result:
 import "encoding/json"
diff --git a/pkg/encoding/yaml/testdata/gen.txtar b/pkg/encoding/yaml/testdata/gen.txtar
index 034ef27e4..846b9a3b4 100644
--- a/pkg/encoding/yaml/testdata/gen.txtar
+++ b/pkg/encoding/yaml/testdata/gen.txtar
@@ -14,9 +14,13 @@ t8: yaml.Marshal({b: int | *2})
 t9: yaml.MarshalStream([{a: 1}, {b: int | *2}])
 -- out/yaml --
 Errors:
-a: error in call to encoding/yaml.Validate: invalid value 4 (out of bound <3)
-a: error in call to encoding/yaml.ValidatePartial: invalid value 4 (out of bound <3)
 b: error in call to encoding/yaml.Validate: incomplete value int
+a: error in call to encoding/yaml.Validate: invalid value 4 (out of bound <3):
+    ./in.cue:3:41
+    yaml.Validate:3:5
+a: error in call to encoding/yaml.ValidatePartial: invalid value 4 (out of bound <3):
+    ./in.cue:6:48
+    yaml.ValidatePartial:3:5
 
 Result:
 t1: _|_ // error in call to encoding/yaml.Validate: a: invalid value 4 (out of bound <3) (and 1 more errors)
diff --git a/pkg/list/testdata/gen.txtar b/pkg/list/testdata/gen.txtar
index 90269d4ce..bb1a9ac2c 100644
--- a/pkg/list/testdata/gen.txtar
+++ b/pkg/list/testdata/gen.txtar
@@ -65,7 +65,6 @@ t54: list.Sort([{a:1}, {b:2}], list.Ascending)
 Errors:
 error in call to list.Avg: empty list
 error in call to list.Drop: negative index
-error in call to list.FlattenN: cannot use value "foo" (type string) as list
 error in call to list.Max: empty list
 error in call to list.Min: empty list
 error in call to list.Range: end must be greater than start when step is positive
@@ -76,13 +75,23 @@ error in call to list.Slice: negative index
 error in call to list.Slice: slice bounds out of range
 error in call to list.Take: negative index
 Ascending.x: error in call to list.Sort: 2 errors in empty disjunction:
-Ascending.x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
-Ascending.x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
+Ascending.x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct):
+    ./in.cue:46:24
+    list:10:9
+Ascending.x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct):
+    ./in.cue:46:24
+    list:10:18
 Ascending.y: error in call to list.Sort: 2 errors in empty disjunction:
-Ascending.y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
-Ascending.y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
+Ascending.y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct):
+    ./in.cue:46:17
+    list:10:9
+Ascending.y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct):
+    ./in.cue:60:17
+    list:10:18
 t3: cannot use "foo" (type string) as list in argument 1 to list.Avg:
     ./in.cue:5:14
+error in call to list.FlattenN: cannot use value "foo" (type string) as list:
+    ./in.cue:15:20
 t14: cannot use "foo" (type string) as int in argument 2 to list.FlattenN:
     ./in.cue:16:24
 t17: cannot use "foo" (type string) as list in argument 1 to list.Max:
diff --git a/pkg/list/testdata/list.txtar b/pkg/list/testdata/list.txtar
index 05f94e45d..5ebcd3a7d 100644
--- a/pkg/list/testdata/list.txtar
+++ b/pkg/list/testdata/list.txtar
@@ -43,10 +43,11 @@ unique: {
 }
 -- out/list --
 Errors:
-error in call to list.Concat: cannot use value 1 (type int) as list
 error in call to list.Repeat: negative count
 concat.t7.v: cannot use 1 (type int) as list in argument 1 to list.Concat:
     ./in.cue:30:12
+error in call to list.Concat: cannot use value 1 (type int) as list:
+    ./in.cue:31:13
 
 Result:
 repeat: {
diff --git a/pkg/math/testdata/gen.txtar b/pkg/math/testdata/gen.txtar
index 1c516007e..268c96bc8 100644
--- a/pkg/math/testdata/gen.txtar
+++ b/pkg/math/testdata/gen.txtar
@@ -25,9 +25,10 @@ t32: math.Dim(3, 2.5)
 t33: math.Dim(5, 7.2)
 -- out/math --
 Errors:
-error in call to math.Jacobi: big: invalid 2nd argument to Int.Jacobi: need odd integer but got 2000
 t3: cannot call non-function math.Pi (type float):
     ./in.cue:5:5
+error in call to math.Jacobi: big: invalid 2nd argument to Int.Jacobi: need odd integer but got 2000:
+    ./in.cue:6:5
 cannot use 2.0E+400 (type float) as float64 in argument 0 to math.Asin: value was rounded up:
     ./in.cue:8:5