internal/core/adt: fix bottom cycle error

Several problems here

- the evaluation state was not passed properly
- error handling was too aggressive bailing out on a cycle error

The changes are rather subtle, and we really need
a more proper rewrite that is designed specifically
for the new caches, rather than retrofitting the old
algorithm.

Did various related cleanups and also removed a
panic that may appear if the existing code is subtly
changed.

Fixes #667

Change-Id: I7a2f221d727f389ab3aec0b15e399b9c327e9906
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8324
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/testdata/cycle/compbottom.txtar b/cue/testdata/cycle/compbottom.txtar
new file mode 100644
index 0000000..1daa0f4
--- /dev/null
+++ b/cue/testdata/cycle/compbottom.txtar
@@ -0,0 +1,813 @@
+// Comparing against bottom is not officially supported by the spec.
+// In practice it is used for a variety of purposes.
+//
+// TODO: It should really be replaced with two builtins:
+//
+//    - exists(reference): check if a certain field exists.
+//    - isvalid(value):    check if a certain value is valid (recursively).
+//
+// For now it implements something in between these two: it fails if a value
+// resolves to an error, but not necessarily if it does so recursively.
+// Although adding a recursive check is easy, it will break existing
+// configurations, as a recursive evaluation will trigger cycles where these
+// are perhaps not expected.
+
+// To verify these tests, each result should have a field
+//
+//     X: "user@example.com"
+//
+// for the large and medium examples and
+//
+//     X: "message: hello"
+//
+// for the simple example.
+//
+// These are not automatically tested using CUE to avoid interfering with the
+// evaluation.
+
+-- in.cue --
+import (
+	"strconv"
+	"regexp"
+)
+
+simple: {
+    #message: #"^(message: (?P<message>.*))?$"#
+
+
+    p1: {
+        X: "message: hello"
+        #aux: {
+            if Y.message == _|_ {
+                message: ""
+            }
+            if Y.message != _|_ {
+                message: "message: " + Y.message
+            }
+        }
+
+        Y: regexp.FindNamedSubmatch(#message, X)
+        X: #aux.message
+    }
+
+    p2: {
+        #aux: {
+            if Y.message == _|_ {
+                message: ""
+            }
+            if Y.message != _|_ {
+                message: "message: " + Y.message
+            }
+        }
+
+        X: "message: hello"
+        Y: regexp.FindNamedSubmatch(#message, X)
+        X: #aux.message
+    }
+
+    p3: {
+        #aux: {
+            if Y.message == _|_ {
+                message: ""
+            }
+            if Y.message != _|_ {
+                message: "message: " + Y.message
+            }
+        }
+
+        Y: regexp.FindNamedSubmatch(#message, X)
+        X: #aux.message
+        X: "message: hello"
+    }
+}
+
+medium: {
+    #userHostPort: #"^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)$"#
+
+    p1: {
+        Y: {
+            userinfo: "user"
+            host: "example.com"
+        }
+
+        X: #X.userinfo + #X.host
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo + "v"
+            }
+
+            host: #Y.host
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+    }
+
+    p2: {
+        X: #X.userinfo + #X.host
+
+        Y: {
+            userinfo: "user"
+            host: "example.com"
+        }
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo + "v"
+            }
+
+            host: #Y.host
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+    }
+
+    p3: {
+        X: #X.userinfo + #X.host
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+        }
+
+        Y: {
+            userinfo: "user"
+            host: "example.com"
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo + "v"
+            }
+
+            host: #Y.host
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+    }
+
+    p4: {
+        X: #X.userinfo + #X.host
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo + "v"
+            }
+
+            host: #Y.host
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+
+        Y: {
+            userinfo: "user"
+            host: "example.com"
+        }
+    }
+}
+
+large: {
+    #userHostPort: 	#"^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)(:(?P<port>\d+))?$"#
+
+    p1: {
+        Y: {
+            userinfo: "user"
+            host:     "example.com"
+        }
+
+        X: #X.userinfo + #X.host + #X.port
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+
+            if Y.port == _|_ {
+                port: ""
+            }
+            if Y.port != _|_ {
+                port: ":" + strconv.FormatInt(Y.port, 10)
+            }
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo
+            }
+            
+            host: #Y.host
+
+            if #Y.port != _|_ {
+                port: strconv.Atoi(#Y.port)
+            }
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+    }
+
+    p2: {
+        X: #X.userinfo + #X.host + #X.port
+
+        Y: {
+            userinfo: "user"
+            host:     "example.com"
+        }
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+
+            if Y.port == _|_ {
+                port: ""
+            }
+            if Y.port != _|_ {
+                port: ":" + strconv.FormatInt(Y.port, 10)
+            }
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo
+            }
+            
+            host: #Y.host
+
+            if #Y.port != _|_ {
+                port: strconv.Atoi(#Y.port)
+            }
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+    }
+
+    p3: {
+        X: #X.userinfo + #X.host + #X.port
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+
+            if Y.port == _|_ {
+                port: ""
+            }
+            if Y.port != _|_ {
+                port: ":" + strconv.FormatInt(Y.port, 10)
+            }
+        }
+
+        Y: {
+            userinfo: "user"
+            host:     "example.com"
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo
+            }
+            
+            host: #Y.host
+
+            if #Y.port != _|_ {
+                port: strconv.Atoi(#Y.port)
+            }
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+    }
+
+    p4: {
+        X: #X.userinfo + #X.host + #X.port
+
+        #X: {
+            if Y.userinfo == _|_ {
+                userinfo: ""
+            }
+            if Y.userinfo != _|_ {
+                userinfo: Y.userinfo + "@"
+            }
+
+            host: Y.host
+
+            if Y.port == _|_ {
+                port: ""
+            }
+            if Y.port != _|_ {
+                port: ":" + strconv.FormatInt(Y.port, 10)
+            }
+        }
+
+        #Y: regexp.FindNamedSubmatch(#userHostPort, X)
+
+        Y: {
+            userinfo: "user"
+            host:     "example.com"
+        }
+
+        Y: {
+            if #Y.userinfo != _|_ {
+                userinfo: #Y.userinfo
+            }
+
+            host: #Y.host
+
+            if #Y.port != _|_ {
+                port: strconv.Atoi(#Y.port)
+            }
+        }
+    }
+}
+-- out/eval --
+(struct){
+  simple: (struct){
+    #message: (string){ "^(message: (?P<message>.*))?$" }
+    p1: (struct){
+      X: (string){ "message: hello" }
+      #aux: (#struct){
+        message: (string){ "message: hello" }
+      }
+      Y: (struct){
+        message: (string){ "hello" }
+      }
+    }
+    p2: (struct){
+      #aux: (#struct){
+        message: (string){ "message: hello" }
+      }
+      X: (string){ "message: hello" }
+      Y: (struct){
+        message: (string){ "hello" }
+      }
+    }
+    p3: (struct){
+      #aux: (#struct){
+        message: (string){ "message: hello" }
+      }
+      Y: (struct){
+        message: (string){ "hello" }
+      }
+      X: (string){ "message: hello" }
+    }
+  }
+  medium: (struct){
+    #userHostPort: (string){ "^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)$" }
+    p1: (struct){
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+      X: (string){ "user@example.com" }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user" }
+      }
+    }
+    p2: (struct){
+      X: (string){ "user@example.com" }
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user" }
+      }
+    }
+    p3: (struct){
+      X: (string){ "user@example.com" }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+      }
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user" }
+      }
+    }
+    p4: (struct){
+      X: (string){ "user@example.com" }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+      }
+      Y: (struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user" }
+      }
+    }
+  }
+  large: (struct){
+    #userHostPort: (string){ "^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)(:(?P<port>\\d+))?$" }
+    p1: (struct){
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+      X: (string){ "user@example.com" }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+        port: (string){ "" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        port: (string){ "" }
+        userinfo: (string){ "user" }
+      }
+    }
+    p2: (struct){
+      X: (string){ "user@example.com" }
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+        port: (string){ "" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        port: (string){ "" }
+        userinfo: (string){ "user" }
+      }
+    }
+    p3: (struct){
+      X: (string){ "user@example.com" }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+        port: (string){ "" }
+      }
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        port: (string){ "" }
+        userinfo: (string){ "user" }
+      }
+    }
+    p4: (struct){
+      X: (string){ "user@example.com" }
+      #X: (#struct){
+        host: (string){ "example.com" }
+        userinfo: (string){ "user@" }
+        port: (string){ "" }
+      }
+      #Y: (#struct){
+        host: (string){ "example.com" }
+        port: (string){ "" }
+        userinfo: (string){ "user" }
+      }
+      Y: (struct){
+        userinfo: (string){ "user" }
+        host: (string){ "example.com" }
+      }
+    }
+  }
+}
+-- out/compile --
+--- in.cue
+{
+  simple: {
+    #message: "^(message: (?P<message>.*))?$"
+    p1: {
+      X: "message: hello"
+      #aux: {
+        if (〈1;Y〉.message == _|_(explicit error (_|_ literal) in source)) {
+          message: ""
+        }
+        if (〈1;Y〉.message != _|_(explicit error (_|_ literal) in source)) {
+          message: ("message: " + 〈2;Y〉.message)
+        }
+      }
+      Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#message〉, 〈0;X〉)
+      X: 〈0;#aux〉.message
+    }
+    p2: {
+      #aux: {
+        if (〈1;Y〉.message == _|_(explicit error (_|_ literal) in source)) {
+          message: ""
+        }
+        if (〈1;Y〉.message != _|_(explicit error (_|_ literal) in source)) {
+          message: ("message: " + 〈2;Y〉.message)
+        }
+      }
+      X: "message: hello"
+      Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#message〉, 〈0;X〉)
+      X: 〈0;#aux〉.message
+    }
+    p3: {
+      #aux: {
+        if (〈1;Y〉.message == _|_(explicit error (_|_ literal) in source)) {
+          message: ""
+        }
+        if (〈1;Y〉.message != _|_(explicit error (_|_ literal) in source)) {
+          message: ("message: " + 〈2;Y〉.message)
+        }
+      }
+      Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#message〉, 〈0;X〉)
+      X: 〈0;#aux〉.message
+      X: "message: hello"
+    }
+  }
+  medium: {
+    #userHostPort: "^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)$"
+    p1: {
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      X: (〈0;#X〉.userinfo + 〈0;#X〉.host)
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;#Y〉.userinfo + "v")
+        }
+        host: 〈1;#Y〉.host
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+    }
+    p2: {
+      X: (〈0;#X〉.userinfo + 〈0;#X〉.host)
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;#Y〉.userinfo + "v")
+        }
+        host: 〈1;#Y〉.host
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+    }
+    p3: {
+      X: (〈0;#X〉.userinfo + 〈0;#X〉.host)
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+      }
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;#Y〉.userinfo + "v")
+        }
+        host: 〈1;#Y〉.host
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+    }
+    p4: {
+      X: (〈0;#X〉.userinfo + 〈0;#X〉.host)
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;#Y〉.userinfo + "v")
+        }
+        host: 〈1;#Y〉.host
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+    }
+  }
+  large: {
+    #userHostPort: "^((?P<userinfo>[[:alnum:]]*)@)?(?P<host>[[:alnum:].]+)(:(?P<port>\\d+))?$"
+    p1: {
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      X: ((〈0;#X〉.userinfo + 〈0;#X〉.host) + 〈0;#X〉.port)
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+        if (〈1;Y〉.port == _|_(explicit error (_|_ literal) in source)) {
+          port: ""
+        }
+        if (〈1;Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: (":" + 〈import;strconv〉.FormatInt(〈2;Y〉.port, 10))
+        }
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: 〈2;#Y〉.userinfo
+        }
+        host: 〈1;#Y〉.host
+        if (〈1;#Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: 〈import;strconv〉.Atoi(〈2;#Y〉.port)
+        }
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+    }
+    p2: {
+      X: ((〈0;#X〉.userinfo + 〈0;#X〉.host) + 〈0;#X〉.port)
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+        if (〈1;Y〉.port == _|_(explicit error (_|_ literal) in source)) {
+          port: ""
+        }
+        if (〈1;Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: (":" + 〈import;strconv〉.FormatInt(〈2;Y〉.port, 10))
+        }
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: 〈2;#Y〉.userinfo
+        }
+        host: 〈1;#Y〉.host
+        if (〈1;#Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: 〈import;strconv〉.Atoi(〈2;#Y〉.port)
+        }
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+    }
+    p3: {
+      X: ((〈0;#X〉.userinfo + 〈0;#X〉.host) + 〈0;#X〉.port)
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+        if (〈1;Y〉.port == _|_(explicit error (_|_ literal) in source)) {
+          port: ""
+        }
+        if (〈1;Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: (":" + 〈import;strconv〉.FormatInt(〈2;Y〉.port, 10))
+        }
+      }
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: 〈2;#Y〉.userinfo
+        }
+        host: 〈1;#Y〉.host
+        if (〈1;#Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: 〈import;strconv〉.Atoi(〈2;#Y〉.port)
+        }
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+    }
+    p4: {
+      X: ((〈0;#X〉.userinfo + 〈0;#X〉.host) + 〈0;#X〉.port)
+      #X: {
+        if (〈1;Y〉.userinfo == _|_(explicit error (_|_ literal) in source)) {
+          userinfo: ""
+        }
+        if (〈1;Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: (〈2;Y〉.userinfo + "@")
+        }
+        host: 〈1;Y〉.host
+        if (〈1;Y〉.port == _|_(explicit error (_|_ literal) in source)) {
+          port: ""
+        }
+        if (〈1;Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: (":" + 〈import;strconv〉.FormatInt(〈2;Y〉.port, 10))
+        }
+      }
+      #Y: 〈import;regexp〉.FindNamedSubmatch(〈1;#userHostPort〉, 〈0;X〉)
+      Y: {
+        userinfo: "user"
+        host: "example.com"
+      }
+      Y: {
+        if (〈1;#Y〉.userinfo != _|_(explicit error (_|_ literal) in source)) {
+          userinfo: 〈2;#Y〉.userinfo
+        }
+        host: 〈1;#Y〉.host
+        if (〈1;#Y〉.port != _|_(explicit error (_|_ literal) in source)) {
+          port: 〈import;strconv〉.Atoi(〈2;#Y〉.port)
+        }
+      }
+    }
+  }
+}
diff --git a/cuego/examples_test.go b/cuego/examples_test.go
index 9fc4e95..daa7604 100644
--- a/cuego/examples_test.go
+++ b/cuego/examples_test.go
@@ -46,7 +46,7 @@
 	// completed: cuego_test.Sum{A:2, B:6, C:8} (err: <nil>)
 	// 2 errors in empty disjunction:
 	// conflicting values null and {A:2,B:3,C:8} (mismatched types null and struct)
-	// A: conflicting values 5 and 2
+	// B: conflicting values 6 and 3
 }
 
 func ExampleConstrain() {
diff --git a/internal/core/adt/composite.go b/internal/core/adt/composite.go
index 143df54..893ea32 100644
--- a/internal/core/adt/composite.go
+++ b/internal/core/adt/composite.go
@@ -137,7 +137,7 @@
 		}
 		env, src := c.e, c.src
 		c.e, c.src = e, x.Source()
-		v = c.evalState(x, Partial)
+		v = c.evalState(x, Partial) // TODO: should this be Finalized?
 		c.e, c.src = env, src
 		e.cache[x] = v
 	}
diff --git a/internal/core/adt/context.go b/internal/core/adt/context.go
index de18cb0..d730ca2 100644
--- a/internal/core/adt/context.go
+++ b/internal/core/adt/context.go
@@ -338,19 +338,6 @@
 	return c.AddErr(c.Newf(format, args...))
 }
 
-func (c *OpContext) validate(v Value) *Bottom {
-	switch x := v.(type) {
-	case *Bottom:
-		return x
-	case *Vertex:
-		v := c.evaluate(x, Partial)
-		if b, ok := v.(*Bottom); ok {
-			return b
-		}
-	}
-	return nil
-}
-
 type frame struct {
 	env *Environment
 	err *Bottom
@@ -540,6 +527,23 @@
 	return val, true
 }
 
+func (c *OpContext) evaluateRec(env *Environment, x Expr, state VertexStatus) Value {
+	s := c.PushState(env, x.Source())
+
+	val := c.evalState(x, state)
+	if val == nil {
+		// Be defensive: this never happens, but just in case.
+		Assertf(false, "nil return value: unspecified error")
+		val = &Bottom{
+			Code: IncompleteError,
+			Err:  c.Newf("UNANTICIPATED ERROR"),
+		}
+	}
+	_ = c.PopState(s)
+
+	return val
+}
+
 // value evaluates expression v within the current environment. The result may
 // be nil if the result is incomplete. value leaves errors untouched to that
 // they can be collected by the caller.
@@ -604,6 +608,7 @@
 // unifyNode returns a possibly partially evaluated node value.
 //
 // TODO: maybe return *Vertex, *Bottom
+//
 func (c *OpContext) unifyNode(v Expr, state VertexStatus) (result Value) {
 	savedSrc := c.src
 	c.src = v.Source()
@@ -612,16 +617,34 @@
 
 	defer func() {
 		c.errs = CombineErrors(c.src, c.errs, err)
-		// TODO: remove this when we handle errors more principally.
-		if b, ok := result.(*Bottom); ok && c.src != nil &&
-			b.Code == CycleError &&
-			b.Err.Position() == token.NoPos &&
-			len(b.Err.InputPositions()) == 0 {
-			bb := *b
-			bb.Err = errors.Wrapf(b.Err, c.src.Pos(), "")
-			result = &bb
+
+		if v, ok := result.(*Vertex); ok {
+			if b, _ := v.BaseValue.(*Bottom); b != nil {
+				switch b.Code {
+				case IncompleteError:
+				case CycleError:
+					if state == Partial {
+						break
+					}
+					fallthrough
+				default:
+					result = b
+				}
+			}
 		}
-		c.errs = CombineErrors(c.src, c.errs, result)
+
+		// TODO: remove this when we handle errors more principally.
+		if b, ok := result.(*Bottom); ok {
+			if c.src != nil &&
+				b.Code == CycleError &&
+				b.Err.Position() == token.NoPos &&
+				len(b.Err.InputPositions()) == 0 {
+				bb := *b
+				bb.Err = errors.Wrapf(b.Err, c.src.Pos(), "")
+				result = &bb
+			}
+			c.errs = CombineErrors(c.src, c.errs, result)
+		}
 		if c.errs != nil {
 			result = c.errs
 		}
@@ -647,11 +670,7 @@
 
 		if v.BaseValue == nil || v.BaseValue == cycle {
 			// Use node itself to allow for cycle detection.
-			c.Unify(v, state)
-		}
-
-		if b, _ := v.BaseValue.(*Bottom); b != nil && b.Code != IncompleteError {
-			return b
+			c.Unify(v, AllArcs)
 		}
 
 		return v
@@ -791,22 +810,30 @@
 func (c *OpContext) node(orig Node, x Expr, scalar bool, state VertexStatus) *Vertex {
 	// TODO: always get the vertex. This allows a whole bunch of trickery
 	// down the line.
-
-	v := c.unifyNode(x, AllArcs)
+	v := c.unifyNode(x, state)
 
 	v, ok := c.getDefault(v)
 	if !ok {
 		// Error already generated by getDefault.
 		return emptyNode
 	}
+
+	// The two if blocks below are rather subtle. If we have an error of
+	// the sentinel value cycle, we have earlier determined that the cycle is
+	// allowed and that it can be ignored here. Any other CycleError is an
+	// annotated cycle error that could be taken as is.
+	// TODO: do something simpler.
 	if scalar {
-		v = Unwrap(v)
+		if w := Unwrap(v); w != cycle {
+			v = w
+		}
 	}
 
 	node, ok := v.(*Vertex)
-	if ok {
+	if ok && node.BaseValue != cycle {
 		v = node.Value()
 	}
+
 	switch nv := v.(type) {
 	case nil:
 		switch orig.(type) {
diff --git a/internal/core/adt/eval.go b/internal/core/adt/eval.go
index d360086..1f6049e 100644
--- a/internal/core/adt/eval.go
+++ b/internal/core/adt/eval.go
@@ -120,10 +120,9 @@
 		if n.errs != nil && !n.errs.IsIncomplete() {
 			return n.errs
 		}
-		// TODO: consider enabling this
-		// if n.scalar != nil {
-		// 	return n.scalar
-		// }
+		if n.scalar != nil && v.BaseValue == cycle {
+			return n.scalar
+		}
 	}
 
 	switch x := v.BaseValue.(type) {
@@ -1127,9 +1126,8 @@
 	case Evaluator:
 		// Interpolation, UnaryExpr, BinaryExpr, CallExpr
 		// Could be unify?
-		val, complete := ctx.Evaluate(v.Env, v.Expr())
-		if !complete {
-			b, _ := val.(*Bottom)
+		val := ctx.evaluateRec(v.Env, v.Expr(), Partial)
+		if b, ok := val.(*Bottom); ok && b.IsIncomplete() {
 			n.exprs = append(n.exprs, envExpr{v, b})
 			break
 		}
@@ -1707,8 +1705,9 @@
 
 	case arc.Status() == 0:
 	default:
-		// TODO: handle adding to finalized conjunct
-		panic(fmt.Sprintf("unhandled %d", arc.status))
+		n.addErr(ctx.NewPosf(pos(x.Field()),
+			"cannot add field %s: was already used",
+			f.SelectorString(ctx)))
 	}
 	return arc
 }
diff --git a/internal/core/adt/expr.go b/internal/core/adt/expr.go
index 4551069..ac9b5ed 100644
--- a/internal/core/adt/expr.go
+++ b/internal/core/adt/expr.go
@@ -1064,31 +1064,22 @@
 		return nil
 	}
 
-	left, _ := c.Concrete(env, x.X, x.Op)
-	right, _ := c.Concrete(env, x.Y, x.Op)
-
-	leftKind := kind(left)
-	rightKind := kind(right)
-
 	// TODO: allow comparing to a literal Bottom only. Find something more
 	// principled perhaps. One should especially take care that two values
 	// evaluating to Bottom don't evaluate to true. For now we check for
 	// Bottom here and require that one of the values be a Bottom literal.
-	if isLiteralBottom(x.X) || isLiteralBottom(x.Y) {
-		if b := c.validate(left); b != nil {
-			left = b
+	if x.Op == EqualOp || x.Op == NotEqualOp {
+		if isLiteralBottom(x.X) {
+			return c.validate(env, x.Src, x.Y, x.Op)
 		}
-		if b := c.validate(right); b != nil {
-			right = b
-		}
-		switch x.Op {
-		case EqualOp:
-			return &Bool{x.Src, leftKind == rightKind}
-		case NotEqualOp:
-			return &Bool{x.Src, leftKind != rightKind}
+		if isLiteralBottom(x.Y) {
+			return c.validate(env, x.Src, x.X, x.Op)
 		}
 	}
 
+	left, _ := c.Concrete(env, x.X, x.Op)
+	right, _ := c.Concrete(env, x.Y, x.Op)
+
 	if err := CombineErrors(x.Src, left, right); err != nil {
 		return err
 	}
@@ -1100,6 +1091,24 @@
 	return BinOp(c, x.Op, left, right)
 }
 
+func (c *OpContext) validate(env *Environment, src ast.Node, x Expr, op Op) Value {
+	s := c.PushState(env, src)
+	defer c.PopState(s)
+
+	for v := c.evalState(x, Partial); ; {
+		switch x := v.(type) {
+		case *Vertex:
+			v, _ = x.BaseValue.(Value)
+
+		case *Bottom:
+			return &Bool{src, op == EqualOp}
+
+		default:
+			return &Bool{src, op != EqualOp}
+		}
+	}
+}
+
 // A CallExpr represents a call to a builtin.
 //
 //    len(x)
diff --git a/pkg/internal/context.go b/pkg/internal/context.go
index 8f0e1af..c428b5a 100644
--- a/pkg/internal/context.go
+++ b/pkg/internal/context.go
@@ -163,6 +163,7 @@
 }
 
 func (c *CallCtxt) String(i int) string {
+	// TODO: use Evaluate instead.
 	x := cue.MakeValue(c.ctx, c.args[i])
 	v, err := x.String()
 	if err != nil {