cue: treat cycle errors as normal incomplete errors

Reference cycles are no different from any other
non-concrete values that can still be resolved.
Reflect this accordingly.

Not doing so can cause unfication to fail in the
validation step done after it.

Add an option to explicitly disallow cycles in
Validate. This may be useful if applications
want to ensure there are no cycles but otherwise
don't care about the precense of non-concrete
values.

Change-Id: Ib718081067e230d8eabd37b9169e292cefec8536
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2708
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/eval.go b/cmd/cue/cmd/eval.go
index 21e08fd..a8e667d 100644
--- a/cmd/cue/cmd/eval.go
+++ b/cmd/cue/cmd/eval.go
@@ -116,31 +116,31 @@
 
 		if exprs == nil {
 			v := inst.Value()
-			if flagConcrete.Bool(cmd) {
+			if flagConcrete.Bool(cmd) && !flagIgnore.Bool(cmd) {
 				if err := v.Validate(cue.Concrete(true)); err != nil {
 					exitIfErr(cmd, inst, err, false)
 					continue
 				}
 			}
 			b, _ := format.Node(getSyntax(v, syn), opts...)
-			w.Write(b)
+			_, _ = w.Write(b)
 		}
 		for _, e := range exprs {
 			if len(exprs) > 1 {
 				fmt.Fprint(w, "// ")
 				b, _ := format.Node(e)
-				w.Write(b)
+				_, _ = w.Write(b)
 				fmt.Fprintln(w)
 			}
 			v := inst.Eval(e)
-			if flagConcrete.Bool(cmd) {
+			if flagConcrete.Bool(cmd) && !flagIgnore.Bool(cmd) {
 				if err := v.Validate(cue.Concrete(true)); err != nil {
 					exitIfErr(cmd, inst, err, false)
 					continue
 				}
 			}
 			b, _ := format.Node(getSyntax(v, syn), opts...)
-			w.Write(b)
+			_, _ = w.Write(b)
 			fmt.Fprintln(w)
 		}
 	}
diff --git a/cue/errors.go b/cue/errors.go
index 73b54ae..12342c9 100644
--- a/cue/errors.go
+++ b/cue/errors.go
@@ -102,7 +102,7 @@
 
 func isIncomplete(v value) bool {
 	if err, ok := v.(*bottom); ok {
-		return err.code == codeIncomplete
+		return err.code == codeIncomplete || err.code == codeCycle
 	}
 	return false
 }
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 920465d..eaf637f 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -466,9 +466,9 @@
 
 			c: [c[1], c[0]]
 		`,
-		out: `<0>{a: _|_((<1>.b - 100):cycle detected), ` +
-			`b: _|_((<1>.a + 100):cycle detected), ` +
-			`c: [_|_(<1>.c[1]:cycle detected),_|_(<1>.c[0]:cycle detected)]}`,
+		out: `<0>{a: (<1>.b - 100), ` +
+			`b: (<1>.a + 100), ` +
+			`c: [<1>.c[1],<1>.c[0]]}`,
 	}, {
 		desc: "resolved self-reference cycles",
 		in: `
@@ -1054,8 +1054,8 @@
 		b: a & { l: [_, "bar"] }
 		`,
 		out: `<0>{` +
-			`a: <1>{l: ["foo",_|_(<2>.v:cycle detected)], ` +
-			`v: _|_(<2>.l[1]:cycle detected)}, ` +
+			`a: <1>{l: ["foo",<2>.v], ` +
+			`v: <2>.l[1]}, ` +
 			`b: <3>{l: ["foo","bar"], v: "bar"}}`,
 	}, {
 		desc: "correct error messages",
@@ -1929,9 +1929,9 @@
 		c2: (*{a:2} | {b:2}) & c1
 		`,
 		out: `<0>{` +
-			`a1: _|_(((*0 | 1) & (<1>.a3 - <1>.a2)):cycle detected), ` +
+			`a1: ((*0 | 1) & (<1>.a3 - <1>.a2)), ` +
 			`a3: 1, ` +
-			`a2: _|_(((*0 | 1) & (<1>.a3 - <1>.a1)):cycle detected), ` +
+			`a2: ((*0 | 1) & (<1>.a3 - <1>.a1)), ` +
 			`b1: (0 | 1), ` +
 			`b2: (0 | 1), ` +
 			`c1: (<2>{a: 1, b: 2} | <3>{a: 2, b: 1}), ` +
diff --git a/cue/types.go b/cue/types.go
index 3f4fa3c..cb39921 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -1346,12 +1346,13 @@
 }
 
 type options struct {
-	concrete     bool // enforce that values are concrete
-	raw          bool // show original values
-	hasHidden    bool
-	omitHidden   bool
-	omitOptional bool
-	omitAttrs    bool
+	concrete       bool // enforce that values are concrete
+	raw            bool // show original values
+	hasHidden      bool
+	omitHidden     bool
+	omitOptional   bool
+	omitAttrs      bool
+	disallowCycles bool // implied by concrete
 }
 
 // An Option defines modes of evaluation.
@@ -1380,6 +1381,12 @@
 	}
 }
 
+// DisallowCycles forces validation in the precense of cycles, even if
+// non-concrete values are allowed. This is implied by Concrete(true).
+func DisallowCycles(disallow bool) Option {
+	return func(p *options) { p.disallowCycles = disallow }
+}
+
 // All indicates that all fields and values should be included in processing
 // even if they can be elided or omitted.
 func All() Option {
@@ -1430,7 +1437,10 @@
 	var errs errors.Error
 	v.Walk(func(v Value) bool {
 		if err := v.checkKind(v.ctx(), bottomKind); err != nil {
-			if !o.concrete && isIncomplete(v.eval(v.ctx())) {
+			if !o.concrete && isIncomplete(err) {
+				if o.disallowCycles && err.code == codeCycle {
+					errs = errors.Append(errs, v.toErr(err))
+				}
 				return false
 			}
 			errs = errors.Append(errs, v.toErr(err))
diff --git a/cue/types_test.go b/cue/types_test.go
index 326a70c..b5c9ab7 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1092,6 +1092,22 @@
 		in:   `a: [{b: string}, 3]`,
 		opts: []Option{Concrete(true)},
 		err:  true,
+	}, {
+		desc: "allow cycles",
+		in: `
+			a: b - 100
+			b: a + 100
+			c: [c[1], c[0]]
+			`,
+	}, {
+		desc: "disallow cycles",
+		in: `
+			a: b - 100
+			b: a + 100
+			c: [c[1], c[0]]
+			`,
+		opts: []Option{DisallowCycles(true)},
+		err:  true,
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index 0e3439b..b822e6c 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -2315,7 +2315,7 @@
 that `a == e`.
 
 ```
-// Config            Evaluates to
+// Config            Evaluates to (requiring concrete values)
 x: {                  x: {
     a: b + 100            a: _|_ // cycle detected
     b: a - 100            b: _|_ // cycle detected
diff --git a/doc/tutorial/basics/cycle.md b/doc/tutorial/basics/cycle.md
index 10b2206..ed30a91 100644
--- a/doc/tutorial/basics/cycle.md
+++ b/doc/tutorial/basics/cycle.md
@@ -29,7 +29,7 @@
 ```
 
 <!-- result -->
-`$ cue eval -i cycle.cue`
+`$ cue eval -i -c cycle.cue`
 ```
 x: 200
 y: 100