cue: implementation of reference cycle handling
Supported:
- delayed constraints for scalare expressions
- field-level cycles, including disjunctions
Not yet supported:
- reference cycles in lists
- structural cycles
Change-Id: I3300193181b24bbb77447dc7648f9bd81b351f48
diff --git a/cue/binop.go b/cue/binop.go
index dec382e..a637f56 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -36,8 +36,19 @@
}
func binOp(ctx *context, src source, op op, left, right evaluated) (result evaluated) {
- if err := firstBottom(left, right); err != nil {
- return err
+ if isBottom(left) {
+ if op == opUnify && ctx.exprDepth == 0 && cycleError(left) != nil {
+ ctx.cycleErr = true
+ return right
+ }
+ return left
+ }
+ if isBottom(right) {
+ if op == opUnify && ctx.exprDepth == 0 && cycleError(right) != nil {
+ ctx.cycleErr = true
+ return left
+ }
+ return right
}
leftKind := left.kind()
@@ -53,7 +64,9 @@
left, right = right, left
}
if op != opUnify {
+ ctx.exprDepth++
v := left.binOp(ctx, src, op, right) // may return incomplete
+ ctx.exprDepth--
return v
}
diff --git a/cue/context.go b/cue/context.go
index 99a1e1c..961b3b4 100644
--- a/cue/context.go
+++ b/cue/context.go
@@ -30,6 +30,9 @@
// constraints are to be evaluated at the end values to be evaluated later.
constraints []value
noDelay bool
+ exprDepth int // nesting of expression that are not unification
+ evalDepth int
+ cycleErr bool
// for debug strings
nodeRefs map[scope]string
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index aab3081..f44da59 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -16,7 +16,6 @@
import (
"flag"
- "fmt"
"testing"
"cuelang.org/go/cue/errors"
@@ -70,9 +69,6 @@
root := testResolve(ctx, obj, r)
got := debugStr(ctx, root)
- if v := ctx.processDelayedConstraints(); v != nil {
- got += fmt.Sprintf("\n%s", debugStr(ctx, v))
- }
// Copy the result
if got != tc.out {
@@ -353,29 +349,31 @@
c: [c[1], c[0]]
`,
out: `<0>{a: _|_(cycle detected), b: _|_(cycle detected), c: _|_(cycle detected)}`,
- // }, {
- // desc: "resolved self-reference cycles",
- // in: `
- // a: b - 100
- // b: a + 100
- // b: 200
+ }, {
+ desc: "resolved self-reference cycles",
+ in: `
+ a: b - 100
+ b: a + 100
+ b: 200
- // c: [c[1], a] // TODO: should be allowed
+ c: [c[1], a] // TODO: should be allowed
- // s1: s2 & {a: 1}
- // s2: s3 & {b: 2}
- // s3: s1 & {c: 3}
- // `,
- // out: `<0>{a: 100, b: 200, c: _|_(cycle detected)}`,
+ s1: s2 & {a: 1}
+ s2: s3 & {b: 2}
+ s3: s1 & {c: 3}
+ `,
+ out: `<0>{a: 100, b: 200, c: _|_(cycle detected), s1: <1>{a: 1, b: 2, c: 3}, s2: <2>{a: 1, b: 2, c: 3}, s3: <3>{a: 1, b: 2, c: 3}}`,
}, {
desc: "delayed constraint failure",
in: `
a: b - 100
b: a + 110
b: 200
+
+ x: 100
+ x: x + 1
`,
- out: `<0>{a: 100, b: 200}
-_|_(((<1>.a + 110) & 200):constraint violated: _|_((210 & 200):cannot unify numbers 210 and 200))`,
+ out: `<0>{a: _|_(((<1>.a + 110) & 200):constraint violated: _|_((210 & 200):cannot unify numbers 210 and 200)), b: 200, x: _|_((100 & (<1>.x + 1)):constraint violated: _|_((100 & 101):cannot unify numbers 100 and 101))}`,
// TODO: find a way to mark error in data.
}}
rewriteHelper(t, testCases, evalPartial)
@@ -800,26 +798,24 @@
`,
out: `<0>{obj: <1>{<>: <2>(Name: string)-><3>{a: ("dummy" | string) if true yield ("sub"): <4>{as: <3>.a}}, ` +
`foo: <5>{a: "bar", sub: <6>{as: "bar"}}}}`,
- // TODO(P3): if two fields are evaluating to the same field, their
- // values could be bound.
- // nodes: use a shared node
- // other: change to where clause
- // unknown: change to where clause.
- // in: `
- // a: b
- // b: a
- // a: { d: 1 }
- // `,
- // out: `<0>{a: {d:1}, b: {d:1}}`,
-
- // TODO(P2): circular references in expressions can be resolved when
- // unified with complete values.
- // in: `
- // a: 20
- // a: b + 10 // 20 & b+10 ==> 20; where a == b+10
- // b: a - 10 // 10-10 = 10 ==> 20 == 10+10
- // `
- // out: `<0>{a_0:20, b_1:10}`
+ }, {
+ desc: "self-reference cycles conflicts with strings",
+ in: `
+ a: {
+ x: y+"?"
+ y: x+"!"
+ }
+ a x: "hey"
+ `,
+ out: `<1>{a: <2>{x: _|_(((<0>.y + "?") & "hey"):constraint violated: _|_("hey":delayed constraint ((<0>.y + "?") & "hey") violated)), y: _|_(cycle detected)}}`,
+ }, {
+ desc: "resolved self-reference cycles with disjunctions",
+ in: `
+ a: b&{x:1} | {y:1} // {x:1,y:3,z:2} | {y:1}
+ b: {x:2} | c&{z:2} // {x:2} | {x:1,y:3,z:2}
+ c: a&{y:3} | {z:3} // {x:1,y:3,z:2} | {z:3}
+ `,
+ out: `<0>{a: (<1>{x: 1, y: 3, z: 2} | <2>{y: 1}), b: (<3>{x: 2} | <4>{x: 1, y: 3, z: 2}), c: (<5>{x: 1, y: 3, z: 2} | <6>{z: 3})}`,
}}
rewriteHelper(t, testCases, evalPartial)
}
diff --git a/cue/value.go b/cue/value.go
index 4397b74..3be1282 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -601,86 +601,6 @@
return v, x.arcs[i].v, x.arcs[i].feature // TODO: return template & v for original?
}
-// Cycle Handling
-//
-// Structs (and thus lists)
-//
-// Unresolved cycle
-// a: b // _|_
-// b: a // _|_
-//
-// a: b | {c:1} // _|_
-// b: a | {d:1} // _|_
-//
-// Resolved cycles
-// a: b // {c:1} : b -> a&{c:1} ->a&{c:1}[a:cycle error] -> {c:1}
-// b: a & { c: 1 } // {c:1}
-//
-// a: b & { d: 1 } // {c:1, d:1}
-// b: a & { c: 1 } // {c:1, d:1}
-//
-// To resolve cycles in disjunctions, the unification operations need to be
-// rewritten before that disjunction values are unified. This is optional for
-// CUE implementations.
-// a: b | {d:1} // {c:1} | {d:1}
-// b: a & {c:1} // b & {c:1} | {d:1}&{c:1} -> {c:1} | {c:1,d:1}
-//
-//
-// Atoms
-// a: b + 100 // _|_
-// b: a - 100 // _|_
-//
-// a: b + 100 // 200
-// b: a - 100 // 100 + check 100 & (a-100)
-// b: 100
-//
-// a: b+100 | b+50 // 200 | 150
-// b: a-100 | a-50 // 100 + check 100 & (a-100 | a-50)
-// b: 100
-
-// TODO: Question: defining an interpration of a constraint structure with no
-// default resolution applied versus one there is. And given a substitution that
-// is valid for both interpretation, resulting in a an b, respectively. Is there
-// any expression in which a an b can be substituted for which the fully
-// evaluated interpretation can yield different results? If so, can we define
-// disjunction in a way that it does not?
-// ANSWER: NO. But is it sufficient to only require this for direct references?
-// Maybe, but at least it holds in that case.
-// f1: a1 | ... | an
-// f2: f1
-// f2: b1 | ... | bm | ... | bn
-// -- Suppose the element that unifies with a1 in f2 is bm.
-// Is a1 & bm the same answer if f2 referencing f1 referred to the
-// disjunction?
-// Assume that some ax, x > 1 unifies with a by, y < m.
-// This would be an ambiguous disjunction, and by definition invalid.
-// The first valid disjunction is one where bm is the first to match
-// any of the values in f1. The result will therefore be the same.
-//
-// Case 1
-// replicas: 1 | int
-//
-// foo: 1 | 2 // interpretations must be equal or else the value crosses
-// foo: replicas
-//
-// Case 2
-// a: {
-// replicas: 1 | int
-// b: {chk: 0, num: 4 } | { chk: int, e: 5, f: int } | { chk: 1, e: 6, f: 7 }
-// b chk: replicas // { chk: 1, num: 5 }
-// c: 1/(replicas-1) | 0 // 0
-// }
-//
-// b: {
-// b chk f: 7
-// }
-//
-// This may be an issue, but by defining all expressions to be lazily evaluated,
-// this is not a surprise. We could have a cue.Choose() builtin to pick
-// the disjunctions recursively. Note that within a single struct, though, the
-// selection for a field is always unique. It may only vary if a substructure
-// is referred to in a new substructure.
-
func (x *structLit) at(ctx *context, i int) evaluated {
x.expand(ctx)
// if x.emit != nil && isBottom(x.emit) {
@@ -693,15 +613,31 @@
// cycle detection
x.arcs[i].cache = cycleSentinel
- v := x.arcs[i].v.evalPartial(ctx)
- if err := cycleError(v); err != nil {
- x.arcs[i].cache = nil
- return err
- }
+ ctx.evalDepth++
+ v := x.arcs[i].v.evalPartial(ctx)
+ ctx.evalDepth--
v = x.applyTemplate(ctx, i, v)
+
+ if ctx.evalDepth > 0 && ctx.cycleErr {
+ // Don't cache while we're in a evaluation cycle as it will cache
+ // partial results. Each field involved in the cycle will have to
+ // reevaluated the values from scratch. As the result will be
+ // cached after one cycle, it will evaluate the cycle at most twice.
+ x.arcs[i].cache = nil
+ return v
+ }
+ // If there as a cycle error, we have by now evaluated full cycle and
+ // it is safe to cache the result.
+ ctx.cycleErr = false
+
x.arcs[i].cache = v
+ if ctx.evalDepth == 0 {
+ if err := ctx.processDelayedConstraints(); err != nil {
+ x.arcs[i].cache = err
+ }
+ }
}
return x.arcs[i].cache
}
@@ -1056,7 +992,12 @@
k := 0
outer:
for i, v := range x.values {
- if isBottom(v.val) {
+ // TODO: this is pre-evaluation is quite aggressive. Verify whether
+ // this does not trigger structural cycles. If so, this can check for
+ // bottom and the validation can be delayed to as late as picking
+ // defaults. The drawback of this approach is that printed intermediate
+ // results will not look great.
+ if err := validate(ctx, v.val); err != nil {
continue
}
for j, w := range x.values {