internal/core/eval: handle structural cycles
Issue #29
Issue #42
Change-Id: I86191d7bf4fdcf8705d740171aedd81e04819dd5
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6522
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/testdata/cycle/015_reference_across_tuples_and_back.txtar b/cue/testdata/cycle/015_reference_across_tuples_and_back.txtar
index 4550cd4..1496ff2 100644
--- a/cue/testdata/cycle/015_reference_across_tuples_and_back.txtar
+++ b/cue/testdata/cycle/015_reference_across_tuples_and_back.txtar
@@ -54,6 +54,6 @@
}
b: (struct){
e: (int){ 3 }
- f: (_){ _ }
+ f: (int){ 3 }
}
}
diff --git a/cue/testdata/cycle/023_reentrance.txtar b/cue/testdata/cycle/023_reentrance.txtar
index 1b6596e..57913b6 100644
--- a/cue/testdata/cycle/023_reentrance.txtar
+++ b/cue/testdata/cycle/023_reentrance.txtar
@@ -79,7 +79,8 @@
}).out
}
-- out/eval --
-(struct){
+(_|_){
+ // [structural cycle]
fibRec: (struct){
nn: (int){ int }
out: (_|_){
@@ -93,6 +94,10 @@
n: (int){ int }
}
fib2: (int){ 1 }
- fib7: (int){ 13 }
- fib12: (int){ 144 }
+ fib7: (_|_){
+ // [structural cycle]
+ }
+ fib12: (_|_){
+ // [structural cycle]
+ }
}
diff --git a/cue/testdata/cycle/025_cannot_resolve_references_that_would_be_ambiguous.txtar b/cue/testdata/cycle/025_cannot_resolve_references_that_would_be_ambiguous.txtar
index 94aba3e..412f416 100644
--- a/cue/testdata/cycle/025_cannot_resolve_references_that_would_be_ambiguous.txtar
+++ b/cue/testdata/cycle/025_cannot_resolve_references_that_would_be_ambiguous.txtar
@@ -56,22 +56,26 @@
-- out/eval --
(struct){
a1: (_|_){
- // [incomplete]
+ // [incomplete] incomplete cause disjunction
}
a2: (_|_){
- // [incomplete]
+ // [incomplete] incomplete cause disjunction
}
a3: (int){ 1 }
- b1: (_|_){
- // [incomplete] ambiguous disjunction
- }
- b2: (_|_){
- // [incomplete] ambiguous disjunction
- }
- c1: (_|_){
- // [incomplete] ambiguous disjunction
- }
- c2: (_|_){
- // [incomplete] ambiguous disjunction
- }
+ b1: (int){ |((int){ 0 }, (int){ 1 }) }
+ b2: (int){ |((int){ 1 }, (int){ 0 }) }
+ c1: (struct){ |((struct){
+ a: (int){ 1 }
+ b: (int){ 2 }
+ }, (struct){
+ b: (int){ 1 }
+ a: (int){ 2 }
+ }) }
+ c2: (struct){ |((struct){
+ a: (int){ 2 }
+ b: (int){ 1 }
+ }, (struct){
+ b: (int){ 2 }
+ a: (int){ 1 }
+ }) }
}
diff --git a/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar b/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar
index 1381f59..e346530 100644
--- a/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar
+++ b/cue/testdata/cycle/051_resolved_self-reference_cycles_with_disjunction.txtar
@@ -169,10 +169,14 @@
xa2: (int){ 8 }
xa3: (int){ 6 }
xa4: (int){ 10 }
- xb1: (int){ 8 }
- xb2: (int){ 8 }
- xb3: (int){ 6 }
- xb4: (int){ 10 }
+ xb1: (int){ |((int){ 8 }, (int){ 9 }) }
+ xb2: (_|_){
+ // [incomplete]
+ }
+ xb3: (int){ |((int){ 6 }, (int){ 9 }) }
+ xb4: (_|_){
+ // [cycle] cycle error
+ }
xc1: (int){ |((int){ 8 }, (int){ 9 }) }
xc2: (int){ 8 }
xc3: (int){ 6 }
@@ -202,10 +206,14 @@
// [eval] incompatible values 7 and 6:
// ./in.cue:44:6
}
- xf1: (int){ 8 }
- xf2: (int){ 8 }
- xf3: (int){ 6 }
- xf4: (int){ 10 }
+ xf1: (int){ |((int){ 8 }, (int){ 9 }) }
+ xf2: (_|_){
+ // [incomplete]
+ }
+ xf3: (int){ |((int){ 6 }, (int){ 9 }) }
+ xf4: (_|_){
+ // [cycle] cycle error
+ }
z1: (int){ |((int){ 11 }, (int){ 13 }) }
z2: (int){ 10 }
z3: (int){ 8 }
diff --git a/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar b/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar
index 8ff1abb..6295a4f 100644
--- a/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar
+++ b/cue/testdata/cycle/052_resolved_self-reference_cycles_with_disjunction_with_defaults.txtar
@@ -139,10 +139,14 @@
xa2: (int){ 8 }
xa3: (int){ 6 }
xa4: (int){ 10 }
- xb1: (int){ 8 }
- xb2: (int){ 8 }
+ xb1: (int){ |(*(int){ 8 }, (int){ 9 }) }
+ xb2: (_|_){
+ // [incomplete]
+ }
xb3: (int){ 6 }
- xb4: (int){ 10 }
+ xb4: (_|_){
+ // [cycle] cycle error
+ }
xc1: (int){ |(*(int){ 8 }, (int){ 9 }) }
xc2: (int){ 8 }
xc3: (int){ 6 }
diff --git a/cue/testdata/cycle/disjunction.txtar b/cue/testdata/cycle/disjunction.txtar
new file mode 100644
index 0000000..accdcfe
--- /dev/null
+++ b/cue/testdata/cycle/disjunction.txtar
@@ -0,0 +1,150 @@
+-- in.cue --
+
+// cycle is a structural cycle
+cycle: a: cycle
+
+// reference to outside structural cycle
+r1a: cycle | int
+r1b: int | cycle
+
+r2a: cycle | 1
+r2b: 1 | cycle
+
+r3a: cycle | null
+r3b: null | cycle
+
+r4a: cycle | {}
+r4b: {} | cycle
+
+r5a: cycle | []
+r5b: [] | cycle
+
+// reference to ancestor node
+s1a: x: s1a | int
+s1b: x: int | s1b
+
+s2a: x: s2a | 1
+s2b: x: 1 | s2b
+
+s3a: x: s3a | null
+s3b: x: null | s3b
+
+s4a: x: s4a | {}
+s4b: x: {} | s4b
+
+s5a: x: s5a | []
+s5b: x: [] | s5b
+
+
+
+-- out/eval --
+Errors:
+structural cycle
+
+Result:
+(_|_){
+ // [structural cycle]
+ cycle: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle] structural cycle
+ a: (_|_){// 〈1;cycle〉
+ }
+ }
+ }
+ r1a: (int){ int }
+ r1b: (int){ int }
+ r2a: (int){ 1 }
+ r2b: (int){ 1 }
+ r3a: (null){ null }
+ r3b: (null){ null }
+ r4a: (struct){
+ }
+ r4b: (struct){
+ }
+ r5a: (#list){
+ }
+ r5b: (#list){
+ }
+ s1a: (struct){
+ x: (int){ int }
+ }
+ s1b: (struct){
+ x: (int){ int }
+ }
+ s2a: (struct){
+ x: (int){ 1 }
+ }
+ s2b: (struct){
+ x: (int){ 1 }
+ }
+ s3a: (struct){
+ x: (null){ null }
+ }
+ s3b: (struct){
+ x: (null){ null }
+ }
+ s4a: (struct){
+ x: (struct){
+ }
+ }
+ s4b: (struct){
+ x: (struct){
+ }
+ }
+ s5a: (struct){
+ x: (#list){
+ }
+ }
+ s5b: (struct){
+ x: (#list){
+ }
+ }
+}
+-- out/compile --
+--- in.cue
+{
+ cycle: {
+ a: 〈1;cycle〉
+ }
+ r1a: (〈0;cycle〉|int)
+ r1b: (int|〈0;cycle〉)
+ r2a: (〈0;cycle〉|1)
+ r2b: (1|〈0;cycle〉)
+ r3a: (〈0;cycle〉|null)
+ r3b: (null|〈0;cycle〉)
+ r4a: (〈0;cycle〉|{})
+ r4b: ({}|〈0;cycle〉)
+ r5a: (〈0;cycle〉|[])
+ r5b: ([]|〈0;cycle〉)
+ s1a: {
+ x: (〈1;s1a〉|int)
+ }
+ s1b: {
+ x: (int|〈1;s1b〉)
+ }
+ s2a: {
+ x: (〈1;s2a〉|1)
+ }
+ s2b: {
+ x: (1|〈1;s2b〉)
+ }
+ s3a: {
+ x: (〈1;s3a〉|null)
+ }
+ s3b: {
+ x: (null|〈1;s3b〉)
+ }
+ s4a: {
+ x: (〈1;s4a〉|{})
+ }
+ s4b: {
+ x: ({}|〈1;s4b〉)
+ }
+ s5a: {
+ x: (〈1;s5a〉|[])
+ }
+ s5b: {
+ x: ([]|〈1;s5b〉)
+ }
+}
diff --git a/cue/testdata/cycle/issue429.txtar b/cue/testdata/cycle/issue429.txtar
index fb9aaff..6e487d1 100644
--- a/cue/testdata/cycle/issue429.txtar
+++ b/cue/testdata/cycle/issue429.txtar
@@ -1,72 +1,168 @@
+TODO: a bound value resolving to a disjunction should probably
+be an error. In this case #Size.amx should resolve.
+
-- in.cue --
-range1: {
- min: *1 | int
- range: >min
- range: 8
-}
-range2: {
- min: *1 | int
- max: int & >min
-}
-rg: range2 & {
-// min: 1
- max: 8
-}
+// Range disjunction without cycle (checks only one-way).
#Size : {
- res: uint | * 0
- num: > res | *(1 + res)
- max: > num | *num
+ res: uint | * 0
+ min: >res | *(1 + res)
+ max: >min | *min
}
-a: #Size & {
- num: 5
+s0: #Size & { res: 1 }
+// This discards the default for max. This is correct, but unfortunate.
+// TODO: is there a tweak to the default mechanism possible that would fix that?
+// Tread very carefully, though! Perhaps we could have a builtin that
+// discards any default, so that we can at least manually override this
+// behavior.
+s1: #Size & { min: 5 }
+s2: #Size & { max: 5 }
+s3: #Size & {
+ min: 5
+ max: 10
}
+es3: #Size & {
+ min: 10
+ max: 5
+}
+
+// Disjunctions with cycles
+// TODO: improve error message here. Logic is correct, though.
+#nonEmptyRange: {
+ min: *1 | int
+ min: <max
+ max: >min
+}
+r1: #nonEmptyRange & {
+ min: 3
+}
+r2: #nonEmptyRange & {
+ max: 5
+}
+r3: #nonEmptyRange & {
+ min: 3
+ max: 6
+}
+
+er3: #nonEmptyRange & {
+ min: 5
+ max: 5
+}
+
+
-- out/eval --
-(struct){
- range1: (struct){
- min: (int){ |(*(int){ 1 }, (int){ int }) }
- range: (int){ 8 }
- }
- range2: (struct){
- min: (int){ |(*(int){ 1 }, (int){ int }) }
- max: (int){ &(>1, int) }
- }
- rg: (struct){
- min: (int){ |(*(int){ 1 }, (int){ int }) }
- max: (int){ 8 }
- }
+Errors:
+invalid value 5 (out of bound <5)
+invalid value 5 (out of bound >10)
+
+Result:
+(_|_){
+ // [eval]
#Size: (#struct){
res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
- num: (number){ |(*(int){ 1 }, (number){ >0 }) }
+ min: (number){ |(*(int){ 1 }, (number){ >0 }) }
max: (number){ |(*(int){ 1 }, (number){ >0 }, (number){ >1 }) }
}
- a: (#struct){
+ s0: (#struct){
+ res: (int){ 1 }
+ min: (number){ |(*(int){ 2 }, (number){ >1 }) }
+ max: (number){ |(*(int){ 2 }, (number){ >1 }, (number){ >2 }) }
+ }
+ s1: (#struct){
res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
- num: (int){ 5 }
+ min: (int){ 5 }
max: (number){ |((int){ 5 }, (number){ >5 }) }
}
+ s2: (#struct){
+ res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
+ min: (number){ |(*(int){ 1 }, (number){ >0 }) }
+ max: (int){ 5 }
+ }
+ s3: (#struct){
+ res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
+ min: (int){ 5 }
+ max: (int){ 10 }
+ }
+ es3: (_|_){
+ // [eval]
+ res: (int){ |(*(int){ 0 }, (int){ &(>=0, int) }) }
+ min: (int){ 10 }
+ max: (_|_){
+ // [eval] invalid value 5 (out of bound >10)
+ }
+ }
+ #nonEmptyRange: (#struct){
+ min: (_|_){
+ // [incomplete] incomplete cause disjunction
+ }
+ max: (_|_){
+ // [incomplete] incomplete cause disjunction
+ }
+ }
+ r1: (#struct){
+ min: (int){ 3 }
+ max: (number){ >3 }
+ }
+ r2: (#struct){
+ min: (int){ |(*(int){ 1 }, (int){ &(<5, int) }) }
+ max: (int){ 5 }
+ }
+ r3: (#struct){
+ min: (int){ 3 }
+ max: (int){ 6 }
+ }
+ er3: (_|_){
+ // [eval]
+ min: (_|_){
+ // [eval] invalid value 5 (out of bound <5)
+ }
+ max: (_|_){
+ // [eval] invalid value 5 (out of bound <5)
+ }
+ }
}
-- out/compile --
--- in.cue
{
- range1: {
- min: (*1|int)
- range: >〈0;min〉
- range: 8
- }
- range2: {
- min: (*1|int)
- max: (int & >〈0;min〉)
- }
- rg: (〈0;range2〉 & {
- max: 8
- })
#Size: {
res: (&(int, >=0)|*0)
- num: (>〈0;res〉|*(1 + 〈0;res〉))
- max: (>〈0;num〉|*〈0;num〉)
+ min: (>〈0;res〉|*(1 + 〈0;res〉))
+ max: (>〈0;min〉|*〈0;min〉)
}
- a: (〈0;#Size〉 & {
- num: 5
+ s0: (〈0;#Size〉 & {
+ res: 1
+ })
+ s1: (〈0;#Size〉 & {
+ min: 5
+ })
+ s2: (〈0;#Size〉 & {
+ max: 5
+ })
+ s3: (〈0;#Size〉 & {
+ min: 5
+ max: 10
+ })
+ es3: (〈0;#Size〉 & {
+ min: 10
+ max: 5
+ })
+ #nonEmptyRange: {
+ min: (*1|int)
+ min: <〈0;max〉
+ max: >〈0;min〉
+ }
+ r1: (〈0;#nonEmptyRange〉 & {
+ min: 3
+ })
+ r2: (〈0;#nonEmptyRange〉 & {
+ max: 5
+ })
+ r3: (〈0;#nonEmptyRange〉 & {
+ min: 3
+ max: 6
+ })
+ er3: (〈0;#nonEmptyRange〉 & {
+ min: 5
+ max: 5
})
}
diff --git a/cue/testdata/cycle/patterns.txtar b/cue/testdata/cycle/patterns.txtar
new file mode 100644
index 0000000..dd25f14
--- /dev/null
+++ b/cue/testdata/cycle/patterns.txtar
@@ -0,0 +1,48 @@
+Lots of cycle-reference goodness.
+
+-- in.cue --
+{[!~"^[.]"]: c}
+a: b
+b: [string]: int
+c: a: int
+{[string]: c}
+a: b
+b: [string]: int
+c: a: int
+
+-- out/eval --
+(struct){
+ a: (struct){
+ a: (int){ int }
+ }
+ b: (struct){
+ a: (int){ int }
+ }
+ c: (struct){
+ a: (int){ int }
+ }
+}
+-- out/compile --
+--- in.cue
+{
+ {
+ [!~"^[.]"]: 〈1;c〉
+ }
+ a: 〈0;b〉
+ b: {
+ [string]: int
+ }
+ c: {
+ a: int
+ }
+ {
+ [string]: 〈1;c〉
+ }
+ a: 〈0;b〉
+ b: {
+ [string]: int
+ }
+ c: {
+ a: int
+ }
+}
diff --git a/cue/testdata/cycle/structural.txtar b/cue/testdata/cycle/structural.txtar
new file mode 100644
index 0000000..32b4d51
--- /dev/null
+++ b/cue/testdata/cycle/structural.txtar
@@ -0,0 +1,1073 @@
+-- in.cue --
+a1: {
+ f: [f]
+}
+
+a2: {
+ f: f
+}
+
+a3: {
+ f: { g: f }
+}
+
+a4: {
+ a: [a|int]
+}
+
+a5: {
+ a: b: a | int
+}
+
+a6: {
+ a: a | int
+}
+
+b1: {
+ b: a & [1]
+ a: [a|int]
+}
+
+b2: {
+ a: [a|int]
+ b: a & [1]
+}
+
+b3: {
+ x: a: [a|int]
+ b: x & {a: [1]}
+}
+
+b4: {
+ b: x.y & [1]
+ x: y: [y]
+}
+
+b5: {
+ b: x.y & {a: [1]}
+ x: y: a: [a|int]
+}
+
+b6: {
+ b: x & {a: [1]}
+ x: a: [a]
+}
+
+// TODO: erroneous: b should be an error. My suspicion is that `a` is detected
+// as a reference cycle, because list don't introduce a new scope, allowing
+// `1` to unify with the equivalent of `a: a` (which normally would be okay).
+b7: {
+ b: a & [[1]]
+ a: [a]
+}
+
+c1: {
+ a: {
+ b: {}
+ c: a & b
+ }
+}
+
+// indirection
+d1: {
+ a: b: c: d: { h: int, t: r }
+ r: a.b
+
+ x: a.b.c
+}
+
+d2: {
+ x: a.b.c
+
+ r: a.b
+ a: b: c: d: { h: int, t: r }
+}
+
+d3: {
+ config: {
+ a: b: c: indirect
+ indirect: [a.b, null][i]
+ i: int | *1
+ }
+ x: config & { i: 0 }
+}
+
+
+// combining structural with reference cycles
+e1: {
+ a: a
+ a: c: a
+
+ b: c: b
+ b: b
+}
+
+e2: {
+ a: {a}
+ a: c: a
+
+ b: c: b
+ b: {b}
+}
+
+e3: {
+ a: [a]
+ a: c: a
+
+ b: [b]
+ b: c: b
+}
+
+e4: {
+ a: [a | {}]
+ a: [[{c: 1}]]
+
+ b: [[{c: 1}]]
+ b: [b | {}]
+}
+
+
+e5: {
+ a: c: a | int
+ a: a | int
+
+ b: b | int
+ b: c: b | int
+}
+
+
+// validating values
+v1: {
+ x: [x | int, 1]
+ y: x & [[[2], 1], 1]
+}
+
+v2: {
+ y: x & [[[2], 1], 1]
+ x: [x | int, 1]
+}
+
+v3: {
+ list: {
+ head: int
+ tail: list | null
+ }
+
+ myList: list
+ myList: {
+ head: 2
+ tail: {
+ head: 3
+ tail: {
+ head: 4
+ }
+ }
+ }
+}
+
+v4: {
+ list: {
+ head: int
+ tail: list | 1
+ }
+
+ myList: list
+ myList: {
+ head: 2
+ tail: head: 3
+ }
+}
+
+v5: {
+ list: {
+ head: int
+ tail: list | {}
+ }
+
+ myList: list
+ myList: {
+ head: 2
+ tail: head: 3
+ }
+}
+
+
+
+// Example from "The Logic of Type Feature Structures" (Bob Carpenter)/
+z1: {
+ y: {
+ f: h: g
+ g: _
+ }
+ x: {
+ f: _
+ g: f
+ }
+ z: x & y
+}
+
+
+// Ensure these are NOT treated as structural errors.
+
+n1: a: b: int
+n2: n1 & { a: n1 }
+n3: n1 & { n1 }
+n4: n1 & { x: n1 & { y: n1 & { z: int }}}
+
+
+-- out/eval --
+Errors:
+conflicting types
+conflicting types list and struct
+structural cycle
+
+Result:
+(_|_){
+ // [eval]
+ a1: (_|_){
+ // [structural cycle]
+ f: (_|_){
+ // [structural cycle]
+ 0: (_|_){
+ // [structural cycle] structural cycle
+ 0: (_|_){// 〈0;f〉
+ }
+ }
+ }
+ }
+ a2: (struct){
+ f: (_){ _ }
+ }
+ a3: (_|_){
+ // [structural cycle]
+ f: (_|_){
+ // [structural cycle]
+ g: (_|_){
+ // [structural cycle] structural cycle
+ g: (_|_){// 〈1;f〉
+ }
+ }
+ }
+ }
+ a4: (struct){
+ a: (#list){
+ 0: (int){ int }
+ }
+ }
+ a5: (struct){
+ a: (struct){
+ b: (int){ int }
+ }
+ }
+ a6: (struct){
+ a: (_){ |((_){ _ }, (int){ int }) }
+ }
+ b1: (struct){
+ b: (#list){
+ 0: (int){ 1 }
+ }
+ a: (#list){
+ 0: (int){ int }
+ }
+ }
+ b2: (struct){
+ a: (#list){
+ 0: (int){ int }
+ }
+ b: (#list){
+ 0: (int){ 1 }
+ }
+ }
+ b3: (struct){
+ x: (struct){
+ a: (#list){
+ 0: (int){ int }
+ }
+ }
+ b: (struct){
+ a: (#list){
+ 0: (int){ 1 }
+ }
+ }
+ }
+ b4: (_|_){
+ // [structural cycle]
+ b: (_|_){
+ // [structural cycle]
+ 0: (int){ 1 }
+ }
+ x: (_|_){
+ // [structural cycle]
+ y: (_|_){
+ // [structural cycle]
+ 0: (_|_){
+ // [structural cycle] structural cycle
+ 0: (_|_){// 〈0;y〉
+ }
+ }
+ }
+ }
+ }
+ b5: (struct){
+ b: (struct){
+ a: (#list){
+ 0: (int){ 1 }
+ }
+ }
+ x: (struct){
+ y: (struct){
+ a: (#list){
+ 0: (int){ int }
+ }
+ }
+ }
+ }
+ b6: (_|_){
+ // [eval]
+ b: (_|_){
+ // [eval]
+ a: (_|_){
+ // [eval]
+ 0: (_|_){
+ // [eval] conflicting types
+ 0: (_|_){
+ // [structural cycle] structural cycle
+ }
+ }
+ }
+ }
+ x: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle]
+ 0: (_|_){
+ // [structural cycle] structural cycle
+ 0: (_|_){// 〈0;a〉
+ }
+ }
+ }
+ }
+ }
+ b7: (_|_){
+ // [structural cycle]
+ b: (#list){
+ 0: (#list){
+ 0: (int){ 1 }
+ }
+ }
+ a: (_|_){
+ // [structural cycle]
+ 0: (_|_){
+ // [structural cycle] structural cycle
+ 0: (_|_){// 〈0;a〉
+ }
+ }
+ }
+ }
+ c1: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle]
+ b: (struct){
+ }
+ c: (_|_){
+ // [structural cycle]
+ b: (struct){
+ }
+ c: (_|_){
+ // [structural cycle] structural cycle
+ b: (_|_){// {}
+ }
+ c: (_|_){// (〈1;a〉 & 〈0;b〉)
+ }
+ }
+ }
+ }
+ }
+ d1: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle]
+ b: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle]
+ d: (_|_){
+ // [structural cycle]
+ h: (int){ int }
+ t: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// {
+ // d: {
+ // h: int
+ // t: 〈4;r〉
+ // }
+ // }
+ }
+ }
+ }
+ }
+ }
+ }
+ r: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// {
+ // d: {
+ // h: int
+ // t: 〈4;r〉
+ // }
+ // }
+ }
+ }
+ x: (_|_){
+ // [structural cycle]
+ }
+ }
+ d2: (_|_){
+ // [structural cycle]
+ x: (_|_){
+ // [structural cycle]
+ }
+ r: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// {
+ // d: {
+ // h: int
+ // t: 〈4;r〉
+ // }
+ // }
+ }
+ }
+ a: (_|_){
+ // [structural cycle]
+ b: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle]
+ d: (_|_){
+ // [structural cycle]
+ h: (int){ int }
+ t: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// {
+ // d: {
+ // h: int
+ // t: 〈4;r〉
+ // }
+ // }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ d3: (_|_){
+ // [structural cycle]
+ config: (struct){
+ a: (struct){
+ b: (struct){
+ c: (null){ null }
+ }
+ }
+ indirect: (null){ null }
+ i: (int){ 1 }
+ }
+ x: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle]
+ b: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// 〈2;indirect〉
+ }
+ }
+ }
+ }
+ indirect: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// 〈2;indirect〉
+ }
+ }
+ i: (int){ 0 }
+ }
+ }
+ e1: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// 〈1;a〉
+ }
+ }
+ }
+ b: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// 〈1;b〉
+ }
+ }
+ }
+ }
+ e2: (_|_){
+ // [structural cycle]
+ a: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// 〈1;a〉
+ }
+ }
+ }
+ b: (_|_){
+ // [structural cycle]
+ c: (_|_){
+ // [structural cycle] structural cycle
+ c: (_|_){// 〈1;b〉
+ }
+ }
+ }
+ }
+ e3: (_|_){
+ // [eval]
+ a: (_|_){
+ // [eval] conflicting types list and struct
+ c: (_|_){
+ // [eval] conflicting types list and struct
+ // structural cycle
+ c: (_|_){// 〈1;a〉
+ }
+ 0: (_|_){// 〈0;a〉
+ }
+ }
+ 0: (_|_){
+ // [eval] conflicting types list and struct
+ // structural cycle
+ c: (_|_){// 〈1;a〉
+ }
+ 0: (_|_){// 〈0;a〉
+ }
+ }
+ }
+ b: (_|_){
+ // [eval] conflicting types list and struct
+ c: (_|_){
+ // [eval] conflicting types list and struct
+ // structural cycle
+ c: (_|_){// 〈1;b〉
+ }
+ 0: (_|_){// 〈0;b〉
+ }
+ }
+ 0: (_|_){
+ // [eval] conflicting types list and struct
+ // structural cycle
+ c: (_|_){// 〈1;b〉
+ }
+ 0: (_|_){// 〈0;b〉
+ }
+ }
+ }
+ }
+ e4: (struct){
+ a: (#list){
+ 0: (#list){
+ 0: (struct){
+ c: (int){ 1 }
+ }
+ }
+ }
+ b: (#list){
+ 0: (#list){
+ 0: (struct){
+ c: (int){ 1 }
+ }
+ }
+ }
+ }
+ e5: (struct){
+ a: (struct){
+ c: (int){ int }
+ }
+ b: (struct){
+ c: (int){ int }
+ }
+ }
+ v1: (struct){
+ x: (#list){
+ 0: (int){ int }
+ 1: (int){ 1 }
+ }
+ y: (#list){
+ 0: (#list){
+ 0: (#list){
+ 0: (int){ 2 }
+ }
+ 1: (int){ 1 }
+ }
+ 1: (int){ 1 }
+ }
+ }
+ v2: (struct){
+ y: (#list){
+ 0: (#list){
+ 0: (#list){
+ 0: (int){ 2 }
+ }
+ 1: (int){ 1 }
+ }
+ 1: (int){ 1 }
+ }
+ x: (#list){
+ 0: (int){ int }
+ 1: (int){ 1 }
+ }
+ }
+ v3: (struct){
+ list: (struct){
+ head: (int){ int }
+ tail: (null){ null }
+ }
+ myList: (struct){
+ head: (int){ 2 }
+ tail: (struct){
+ head: (int){ 3 }
+ tail: (struct){
+ head: (int){ 4 }
+ tail: (null){ null }
+ }
+ }
+ }
+ }
+ v4: (struct){
+ list: (struct){
+ head: (int){ int }
+ tail: (int){ 1 }
+ }
+ myList: (struct){
+ head: (int){ 2 }
+ tail: (struct){
+ head: (int){ 3 }
+ tail: (int){ 1 }
+ }
+ }
+ }
+ v5: (struct){
+ list: (struct){
+ head: (int){ int }
+ tail: (struct){
+ }
+ }
+ myList: (struct){
+ head: (int){ 2 }
+ tail: (struct){ |((struct){
+ head: (int){ 3 }
+ tail: (struct){
+ }
+ }, (struct){
+ head: (int){ 3 }
+ }) }
+ }
+ }
+ z1: (_|_){
+ // [structural cycle]
+ y: (struct){
+ f: (struct){
+ h: (_){ _ }
+ }
+ g: (_){ _ }
+ }
+ x: (struct){
+ f: (_){ _ }
+ g: (_){ _ }
+ }
+ z: (_|_){
+ // [structural cycle]
+ f: (_|_){
+ // [structural cycle]
+ h: (_|_){
+ // [structural cycle]
+ h: (_|_){
+ // [structural cycle] structural cycle
+ h: (_|_){// 〈1;g〉
+ }
+ }
+ }
+ }
+ g: (_|_){
+ // [structural cycle]
+ h: (_|_){
+ // [structural cycle] structural cycle
+ h: (_|_){// 〈1;g〉
+ }
+ }
+ }
+ }
+ }
+ n1: (struct){
+ a: (struct){
+ b: (int){ int }
+ }
+ }
+ n2: (struct){
+ a: (struct){
+ b: (int){ int }
+ a: (struct){
+ b: (int){ int }
+ }
+ }
+ }
+ n3: (struct){
+ a: (struct){
+ b: (int){ int }
+ }
+ }
+ n4: (struct){
+ a: (struct){
+ b: (int){ int }
+ }
+ x: (struct){
+ a: (struct){
+ b: (int){ int }
+ }
+ y: (struct){
+ a: (struct){
+ b: (int){ int }
+ }
+ z: (int){ int }
+ }
+ }
+ }
+}
+-- out/compile --
+--- in.cue
+{
+ a1: {
+ f: [
+ 〈0;f〉,
+ ]
+ }
+ a2: {
+ f: 〈0;f〉
+ }
+ a3: {
+ f: {
+ g: 〈1;f〉
+ }
+ }
+ a4: {
+ a: [
+ (〈0;a〉|int),
+ ]
+ }
+ a5: {
+ a: {
+ b: (〈1;a〉|int)
+ }
+ }
+ a6: {
+ a: (〈0;a〉|int)
+ }
+ b1: {
+ b: (〈0;a〉 & [
+ 1,
+ ])
+ a: [
+ (〈0;a〉|int),
+ ]
+ }
+ b2: {
+ a: [
+ (〈0;a〉|int),
+ ]
+ b: (〈0;a〉 & [
+ 1,
+ ])
+ }
+ b3: {
+ x: {
+ a: [
+ (〈0;a〉|int),
+ ]
+ }
+ b: (〈0;x〉 & {
+ a: [
+ 1,
+ ]
+ })
+ }
+ b4: {
+ b: (〈0;x〉.y & [
+ 1,
+ ])
+ x: {
+ y: [
+ 〈0;y〉,
+ ]
+ }
+ }
+ b5: {
+ b: (〈0;x〉.y & {
+ a: [
+ 1,
+ ]
+ })
+ x: {
+ y: {
+ a: [
+ (〈0;a〉|int),
+ ]
+ }
+ }
+ }
+ b6: {
+ b: (〈0;x〉 & {
+ a: [
+ 1,
+ ]
+ })
+ x: {
+ a: [
+ 〈0;a〉,
+ ]
+ }
+ }
+ b7: {
+ b: (〈0;a〉 & [
+ [
+ 1,
+ ],
+ ])
+ a: [
+ 〈0;a〉,
+ ]
+ }
+ c1: {
+ a: {
+ b: {}
+ c: (〈1;a〉 & 〈0;b〉)
+ }
+ }
+ d1: {
+ a: {
+ b: {
+ c: {
+ d: {
+ h: int
+ t: 〈4;r〉
+ }
+ }
+ }
+ }
+ r: 〈0;a〉.b
+ x: 〈0;a〉.b.c
+ }
+ d2: {
+ x: 〈0;a〉.b.c
+ r: 〈0;a〉.b
+ a: {
+ b: {
+ c: {
+ d: {
+ h: int
+ t: 〈4;r〉
+ }
+ }
+ }
+ }
+ }
+ d3: {
+ config: {
+ a: {
+ b: {
+ c: 〈2;indirect〉
+ }
+ }
+ indirect: [
+ 〈0;a〉.b,
+ null,
+ ][〈0;i〉]
+ i: (int|*1)
+ }
+ x: (〈0;config〉 & {
+ i: 0
+ })
+ }
+ e1: {
+ a: 〈0;a〉
+ a: {
+ c: 〈1;a〉
+ }
+ b: {
+ c: 〈1;b〉
+ }
+ b: 〈0;b〉
+ }
+ e2: {
+ a: {
+ 〈1;a〉
+ }
+ a: {
+ c: 〈1;a〉
+ }
+ b: {
+ c: 〈1;b〉
+ }
+ b: {
+ 〈1;b〉
+ }
+ }
+ e3: {
+ a: [
+ 〈0;a〉,
+ ]
+ a: {
+ c: 〈1;a〉
+ }
+ b: [
+ 〈0;b〉,
+ ]
+ b: {
+ c: 〈1;b〉
+ }
+ }
+ e4: {
+ a: [
+ (〈0;a〉|{}),
+ ]
+ a: [
+ [
+ {
+ c: 1
+ },
+ ],
+ ]
+ b: [
+ [
+ {
+ c: 1
+ },
+ ],
+ ]
+ b: [
+ (〈0;b〉|{}),
+ ]
+ }
+ e5: {
+ a: {
+ c: (〈1;a〉|int)
+ }
+ a: (〈0;a〉|int)
+ b: (〈0;b〉|int)
+ b: {
+ c: (〈1;b〉|int)
+ }
+ }
+ v1: {
+ x: [
+ (〈0;x〉|int),
+ 1,
+ ]
+ y: (〈0;x〉 & [
+ [
+ [
+ 2,
+ ],
+ 1,
+ ],
+ 1,
+ ])
+ }
+ v2: {
+ y: (〈0;x〉 & [
+ [
+ [
+ 2,
+ ],
+ 1,
+ ],
+ 1,
+ ])
+ x: [
+ (〈0;x〉|int),
+ 1,
+ ]
+ }
+ v3: {
+ list: {
+ head: int
+ tail: (〈1;list〉|null)
+ }
+ myList: 〈0;list〉
+ myList: {
+ head: 2
+ tail: {
+ head: 3
+ tail: {
+ head: 4
+ }
+ }
+ }
+ }
+ v4: {
+ list: {
+ head: int
+ tail: (〈1;list〉|1)
+ }
+ myList: 〈0;list〉
+ myList: {
+ head: 2
+ tail: {
+ head: 3
+ }
+ }
+ }
+ v5: {
+ list: {
+ head: int
+ tail: (〈1;list〉|{})
+ }
+ myList: 〈0;list〉
+ myList: {
+ head: 2
+ tail: {
+ head: 3
+ }
+ }
+ }
+ z1: {
+ y: {
+ f: {
+ h: 〈1;g〉
+ }
+ g: _
+ }
+ x: {
+ f: _
+ g: 〈0;f〉
+ }
+ z: (〈0;x〉 & 〈0;y〉)
+ }
+ n1: {
+ a: {
+ b: int
+ }
+ }
+ n2: (〈0;n1〉 & {
+ a: 〈1;n1〉
+ })
+ n3: (〈0;n1〉 & {
+ 〈1;n1〉
+ })
+ n4: (〈0;n1〉 & {
+ x: (〈1;n1〉 & {
+ y: (〈2;n1〉 & {
+ z: int
+ })
+ })
+ })
+}
diff --git "a/cue/testdata/definitions/033_Issue_\043153.txtar" "b/cue/testdata/definitions/033_Issue_\043153.txtar"
index 175494e..ddb37ff 100644
--- "a/cue/testdata/definitions/033_Issue_\043153.txtar"
+++ "b/cue/testdata/definitions/033_Issue_\043153.txtar"
@@ -79,7 +79,7 @@
listOfCloseds: (list){
}
}
- #Closed: (#struct){
+ #Closed: (struct){
a: (int){ |(*(int){ 0 }, (int){ int }) }
}
Junk: (struct){
diff --git a/cue/testdata/export/027.txtar b/cue/testdata/export/027.txtar
index fe2954c..7525d1d 100644
--- a/cue/testdata/export/027.txtar
+++ b/cue/testdata/export/027.txtar
@@ -1,5 +1,9 @@
# DO NOT EDIT; generated by go run testdata/gen.go
#
+
+TODO: Should #Bar result in string or #Foo | string?
+
+
raw: true
eval: true
-- in.cue --
@@ -27,3 +31,9 @@
}
}
}
+-- out/eval --
+(struct){
+ #Foo: (#struct){
+ #Bar: (string){ string }
+ }
+}
diff --git a/cue/testdata/export/028.txtar b/cue/testdata/export/028.txtar
index 5a7a661..70e40f3 100644
--- a/cue/testdata/export/028.txtar
+++ b/cue/testdata/export/028.txtar
@@ -38,3 +38,15 @@
]
}
}
+-- out/eval --
+(struct){
+ #FindInMap: (#struct){
+ #: (#struct){
+ "Fn::FindInMap": (#list){
+ 0: (string){ string }
+ }
+ }
+ }
+ a: (list){
+ }
+}
diff --git a/cue/testdata/export/030.txtar b/cue/testdata/export/030.txtar
index 834a1b7..03e96f0 100644
--- a/cue/testdata/export/030.txtar
+++ b/cue/testdata/export/030.txtar
@@ -53,3 +53,20 @@
#Bar: string
}
}
+-- out/eval --
+(struct){
+ #Foo: (#struct){
+ sgl: (string){ string }
+ ref: ((null|struct)){ |((null){ null }, (#struct){
+ sgl: (string){ string }
+ ref: (null){ null }
+ ext: ((null|string)){ |((string){ string }, (null){ null }) }
+ ref2: ((null|string)){ |((null){ null }, (string){ string }) }
+ "#Foo": (int){ 2 }
+ }) }
+ ext: ((null|string)){ |((string){ string }, (null){ null }) }
+ ref2: ((null|string)){ |((null){ null }, (string){ string }) }
+ "#Foo": (int){ 2 }
+ }
+ #Bar: (string){ string }
+}
diff --git a/internal/core/adt/composite.go b/internal/core/adt/composite.go
index d7c51ca..5e2f611 100644
--- a/internal/core/adt/composite.go
+++ b/internal/core/adt/composite.go
@@ -22,6 +22,61 @@
"cuelang.org/go/cue/token"
)
+// TODO: unanswered questions about structural cycles:
+//
+// 1. When detecting a structural cycle, should we consider this as:
+// a) an unevaluated value,
+// b) an incomplete error (which does not affect parent validity), or
+// c) a special value.
+//
+// Making it an error is the simplest way to ensure reentrancy is disallowed:
+// without an error it would require an additional mechanism to stop reentrancy
+// from continuing to process. Even worse, in some cases it may only partially
+// evaluate, resulting in unexpected results. For this reason, we are taking
+// approach `b` for now.
+//
+// This has some consequences of how disjunctions are treated though. Consider
+//
+// list: {
+// head: _
+// tail: list | null
+// }
+//
+// When making it an error, evaluating the above will result in
+//
+// list: {
+// head: _
+// tail: null
+// }
+//
+// because list will result in a structural cycle, and thus an error, it will be
+// stripped from the disjunction. This may or may not be a desirable property. A
+// nice thing is that it is not required to write `list | *null`. A disadvantage
+// is that this is perhaps somewhat inexplicit.
+//
+// When not making it an error (and simply cease evaluating child arcs upon
+// cycle detection), the result would be:
+//
+// list: {
+// head: _
+// tail: list | null
+// }
+//
+// In other words, an evaluation would result in a cycle and thus an error.
+// Implementations can recognize such cases by having unevaluated arcs. An
+// explicit structure cycle marker would probably be less error prone.
+//
+// Note that in both cases, a reference to list will still use the original
+// conjuncts, so the result will be the same for either method in this case.
+//
+//
+// 2. Structural cycle allowance.
+//
+// Structural cycle detection disallows reentrancy as well. This means one
+// cannot use structs for recursive computation. This will probably preclude
+// evaluation of some configuration. Given that there is no real alternative
+// yet, we could allow structural cycle detection to be optionally disabled.
+
// An Environment links the parent scopes for identifier lookup to a composite
// node. Each conjunct that make up node in the tree can be associated with
// a different environment (although some conjuncts may share an Environment).
@@ -33,10 +88,44 @@
// constraint. It is used to resolve label references.
DynamicLabel Feature
+ // TODO(perf): make the following public fields a shareable struct as it
+ // mostly is going to be the same for child nodes.
+
// CloseID is a unique number that tracks a group of conjuncts that need
// belong to a single originating definition.
CloseID uint32
+ // Cyclic indicates a structural cycle was detected for this conjunct or one
+ // of its ancestors.
+ Cyclic bool
+
+ // Deref keeps track of nodes that should dereference to Vertex. It is used
+ // for detecting structural cycle.
+ //
+ // The detection algorithm is based on Tomabechi's quasi-destructive graph
+ // unification. This detection requires dependencies to be resolved into
+ // fully dereferenced vertices. This is not the case in our algorithm:
+ // the result of evaluating conjuncts is placed into dereferenced vertices
+ // _after_ they are evaluated, but the Environment still points to the
+ // non-dereferenced context.
+ //
+ // In order to be able to detect structural cycles, we need to ensure that
+ // at least one node that is part of a cycle in the context in which
+ // conjunctions are evaluated dereferences correctly.
+ //
+ // The only field necessary to detect a structural cycle, however, is
+ // the Status field of the Vertex. So rather than dereferencing a node
+ // proper, it is sufficient to copy the Status of the dereferenced nodes
+ // to these nodes (will always be EvaluatingArcs).
+ Deref []*Vertex
+
+ // Cycles contains vertices for which cycles are detected. It is used
+ // for tracking self-references within structural cycles.
+ //
+ // Unlike Deref, Cycles is not incremented with child nodes.
+ // TODO: Cycles is always a tail end of Deref, so this can be optimized.
+ Cycles []*Vertex
+
cache map[Expr]Value
}
@@ -71,6 +160,8 @@
// Label is the feature leading to this vertex.
Label Feature
+ // TODO: move the following status fields to a separate struct.
+
// status indicates the evaluation progress of this vertex.
status VertexStatus
@@ -79,6 +170,13 @@
// ignored.
isData bool
+ // EvalCount keeps track of temporary dereferencing during evaluation.
+ // If EvalCount > 0, status should be considered to be EvaluatingArcs.
+ EvalCount int
+
+ // SelfCount is used for tracking self-references.
+ SelfCount int
+
// Value is the value associated with this vertex. For lists and structs
// this is a sentinel value indicating its kind.
Value Value
@@ -140,6 +238,9 @@
)
func (v *Vertex) Status() VertexStatus {
+ if v.EvalCount > 0 {
+ return EvaluatingArcs
+ }
return v.status
}
diff --git a/internal/core/adt/errors.go b/internal/core/adt/errors.go
index 58ddac5..ecfe359 100644
--- a/internal/core/adt/errors.go
+++ b/internal/core/adt/errors.go
@@ -51,6 +51,12 @@
// Mostly used for legacy reasons.
NotExistError
+ // StructuralCycleError means a structural cycle was found. Structural
+ // cycles are permanent errors, but they are not passed up recursively,
+ // as a unification of a value with a structural cycle with one that
+ // doesn't may still give a useful result.
+ StructuralCycleError
+
// IncompleteError means an evaluation could not complete because of
// insufficient information that may still be added later.
IncompleteError
@@ -67,6 +73,8 @@
return "eval"
case UserError:
return "user"
+ case StructuralCycleError:
+ return "structural cycle"
case IncompleteError:
return "incomplete"
case CycleError:
diff --git a/internal/core/eval/eval.go b/internal/core/eval/eval.go
index 596a62d..950f3aa 100644
--- a/internal/core/eval/eval.go
+++ b/internal/core/eval/eval.go
@@ -545,6 +545,8 @@
n.node.Closed = m
}
+ cyclic := n.hasCycle && !n.hasNonCycle
+
// Visit arcs recursively to validate and compute error.
for _, a := range n.node.Arcs {
if updated != nil {
@@ -560,13 +562,24 @@
}
// Call UpdateStatus here to be absolutely sure the status is set
// correctly and that we are not regressing.
- n.node.UpdateStatus(adt.EvaluatingArcs)
- n.eval.Unify(ctx, a, adt.Finalized)
- if err, _ := a.Value.(*adt.Bottom); err != nil {
- n.node.AddChildError(err)
+ if !cyclic {
+ n.node.UpdateStatus(adt.EvaluatingArcs)
+ n.eval.Unify(ctx, a, adt.Finalized)
+ if err, _ := a.Value.(*adt.Bottom); err != nil {
+ n.node.AddChildError(err)
+ }
}
}
+ if cyclic {
+ n.node.Value = adt.CombineErrors(nil, n.node.Value, &adt.Bottom{
+ Code: adt.StructuralCycleError,
+ Err: errors.Newf(token.NoPos, "structural cycle"),
+ Value: n.node.Value,
+ // TODO: probably, this should have the referenced arc.
+ })
+ }
+
n.node.UpdateStatus(adt.Finalized)
}
@@ -682,6 +695,9 @@
vLists []*adt.Vertex
exprs []conjunct
+ hasCycle bool // has conjunct with structural cycle
+ hasNonCycle bool // has conjunct without structural cycle
+
// Disjunction handling
disjunctions []envDisjunct
defaultMode defaultMode
@@ -867,6 +883,23 @@
// Require an Environment.
ctx := n.ctx
+ // TODO: see if we can do without these counters.
+ for _, d := range v.Env.Deref {
+ d.EvalCount++
+ }
+ for _, d := range v.Env.Cycles {
+ d.SelfCount++
+ }
+ defer func() {
+ for _, d := range v.Env.Deref {
+ d.EvalCount--
+ }
+ for _, d := range v.Env.Cycles {
+ d.SelfCount++
+ }
+ }()
+
+outer:
switch x := v.Expr().(type) {
case adt.Resolver:
arc, err := ctx.Resolve(v.Env, x)
@@ -883,30 +916,81 @@
break
}
- // If this is a cycle error, we have reached a fixed point and adding
- // conjuncts at this point will not change the value. Also, continuing
- // to pursue this value will result in an infinite loop.
- //
- // TODO: add a mechanism so that the computation will only have to be
- // one once?
- if isEvaluating(arc) {
- break
+ // Pass detection of structural cycles from parent to children.
+ cyclic := false
+ if v.Env != nil {
+ // If a reference is in a tainted set, so is the value it refers to.
+ cyclic = v.Env.Cyclic
}
- // TODO: detect structural cycles here. A structural cycle can occur
- // if it is not a reference cycle, but refers to a parent. node.
- // This should only be allowed if it is unified with a finite structure.
+ status := arc.Status()
+ for _, d := range v.Env.Deref {
+ if d == arc {
+ status = adt.EvaluatingArcs
+ }
+ }
+
+ switch status {
+ case adt.Evaluating:
+ // Reference cycle detected. We have reached a fixed point and
+ // adding conjuncts at this point will not change the value. Also,
+ // continuing to pursue this value will result in an infinite loop.
+
+ // TODO: add a mechanism so that the computation will only have to
+ // be done once?
+
+ if arc == n.node {
+ // TODO: we could use node sharing here. This may avoid an
+ // exponential blowup during evaluation, like is possible with
+ // YAML.
+ break outer
+ }
+
+ case adt.EvaluatingArcs:
+ // Structural cycle detected. Continue evaluation as usual, but
+ // keep track of whether any other conjuncts without structural
+ // cycles are added. If not, evaluation of child arcs will end
+ // with this node.
+
+ // For the purpose of determining whether at least one non-cyclic
+ // conjuncts exists, we consider all conjuncts of a cyclic conjuncts
+ // also cyclic.
+
+ cyclic = true
+ n.hasCycle = true
+
+ // As the adt.EvaluatingArcs mechanism bypasses the self-reference
+ // mechanism, we need to separately keep track of it here.
+ // If this (originally) is a self-reference node, adding them
+ // will result in recursively adding the same reference. For this
+ // we also mark the node as evaluating.
+ if arc.SelfCount > 0 {
+ break outer
+ }
+
+ // This count is added for values that are directly added below.
+ // The count is handled separately for delayed values.
+ arc.SelfCount++
+ defer func() { arc.SelfCount-- }()
+ }
+
+ // TODO: uncommenting the following almost works, but causes some
+ // faulty results in complex cycle handling between disjunctions.
+ // The reason is that disjunctions must be eliminated if checks in
+ // values on which they depend fail.
+ ctx.Unify(ctx, arc, adt.Finalized)
if arc.Label.IsDef() {
- n.insertClosed(arc)
+ n.insertClosed(arc, cyclic, arc)
} else {
for _, a := range arc.Conjuncts {
- n.addExprConjunct(a, closeID, top)
+ n.addExprConjunct(updateCyclic(a, cyclic, arc), closeID, top)
}
}
case adt.Evaluator:
// adt.Interpolation, adt.UnaryExpr, adt.BinaryExpr, adt.CallExpr
+ // Could be unify?
val, complete := ctx.Evaluate(v.Env, v.Expr())
if !complete {
n.exprs = append(n.exprs, conjunct{v, closeID, top})
@@ -933,7 +1017,40 @@
}
}
-func (n *nodeContext) insertClosed(arc *adt.Vertex) {
+// updateCyclicStatus looks for proof of non-cyclic conjuncts to override
+// a structural cycle.
+func (n *nodeContext) updateCyclicStatus(env *adt.Environment) {
+ if env == nil || !env.Cyclic {
+ // fmt.Printf("%p -- %d hasNonCycle\n", n.node, n.node.Label)
+ n.hasNonCycle = true
+ }
+}
+
+func updateCyclic(c adt.Conjunct, cyclic bool, deref *adt.Vertex) adt.Conjunct {
+ env := c.Env
+ switch {
+ case env == nil:
+ if !cyclic && deref == nil {
+ return c
+ }
+ env = &adt.Environment{Cyclic: cyclic}
+ case deref == nil && env.Cyclic == cyclic:
+ return c
+ default:
+ // The conjunct may still be in use in other fields, so we should
+ // make a new copy to mark Cyclic only for this case.
+ e := *env
+ e.Cyclic = e.Cyclic || cyclic
+ env = &e
+ }
+ if deref != nil {
+ env.Deref = append(env.Deref, deref)
+ env.Cycles = append(env.Cycles, deref)
+ }
+ return adt.MakeConjunct(env, c.Expr())
+}
+
+func (n *nodeContext) insertClosed(arc *adt.Vertex, cyclic bool, deref *adt.Vertex) {
id := n.eval.nextID()
n.needClose = true
@@ -941,7 +1058,7 @@
n.newClose = nil
for _, a := range arc.Conjuncts {
- n.addExprConjunct(a, id, false)
+ n.addExprConjunct(updateCyclic(a, cyclic, deref), id, false)
}
current, n.newClose = n.newClose, current
@@ -953,6 +1070,8 @@
}
func (n *nodeContext) addValueConjunct(env *adt.Environment, v adt.Value) {
+ n.updateCyclicStatus(env)
+
if x, ok := v.(*adt.Vertex); ok {
needClose := false
if isStruct(x) {
@@ -969,13 +1088,14 @@
n.node.AddStructs(x.Structs...)
}
- if len(x.Conjuncts) > 0 {
+ if !x.IsData() && len(x.Conjuncts) > 0 {
+ cyclic := env != nil && env.Cyclic
if needClose {
- n.insertClosed(x)
+ n.insertClosed(x, cyclic, nil)
return
}
for _, c := range x.Conjuncts {
- n.addExprConjunct(c, 0, false) // Pass from eval
+ n.addExprConjunct(updateCyclic(c, cyclic, nil), 0, false) // TODO: Pass from eval
}
return
}
@@ -1112,6 +1232,8 @@
newDef uint32,
top bool) {
+ n.updateCyclicStatus(env) // to handle empty structs.
+
ctx := n.ctx
n.node.AddStructs(s)
@@ -1138,6 +1260,10 @@
Vertex: n.node,
CloseID: closeID,
}
+ if env != nil {
+ childEnv.Cyclic = env.Cyclic
+ childEnv.Deref = env.Deref
+ }
var hasOther, hasBulk adt.Node
@@ -1422,6 +1548,8 @@
outer:
for i, l := range n.lists {
+ n.updateCyclicStatus(l.env)
+
index := int64(0)
hasComprehension := false
for j, elem := range l.list.Elems {
diff --git a/internal/core/eval/eval_test.go b/internal/core/eval/eval_test.go
index 604def6..96197b1 100644
--- a/internal/core/eval/eval_test.go
+++ b/internal/core/eval/eval_test.go
@@ -17,6 +17,7 @@
import (
"flag"
"fmt"
+ "strings"
"testing"
"cuelang.org/go/internal/core/debug"
@@ -24,7 +25,6 @@
"cuelang.org/go/internal/core/validate"
"cuelang.org/go/internal/cuetxtar"
"cuelang.org/go/internal/legacy/cue"
- "cuelang.org/go/pkg/strings"
"github.com/rogpeppe/go-internal/txtar"
)
@@ -85,12 +85,7 @@
}
var needFix = map[string]string{
- "export/027": "cycle",
- "export/028": "cycle",
- "export/030": "cycle",
-
- "cycle/025_cannot_resolve_references_that_would_be_ambiguous": "cycle",
- "compile/scope": "cycle",
+ "DIR/NAME": "reason",
}
// TestX is for debugging. Do not delete.