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 {