internal/diff: avoid cycle when diffing
Also introduce a MaxDepth constant and use it
consistently.
Change-Id: I82d07ae3fdad903382a8291743f0beeabb147ce9
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5460
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/subsume.go b/cue/subsume.go
index 57834a5..05881cb 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -64,6 +64,10 @@
// recorded values where an error occurred.
gt, lt evaluated
missing label
+
+ // depth is used to work around undetected cycles.
+ // TODO(eval): remove once cycle detection is implemented.
+ depth int
}
type subsumeMode int
@@ -98,6 +102,12 @@
// subsumption could be proven. For concreted values it returns the exact
// relation. It never returns a false positive.
func (s *subsumer) subsumes(gt, lt value) (result bool) {
+ if s.depth > internal.MaxDepth {
+ return true
+ }
+ s.depth++
+ defer func() { s.depth-- }()
+
ctx := s.ctx
var v, w evaluated
if s.mode&subChoose == 0 {
diff --git a/cue/types.go b/cue/types.go
index 9b71831..8da7660 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -2013,8 +2013,8 @@
func (x *validator) walk(v Value, opts options) {
// TODO(#42): we can get rid of the arbitrary evaluation depth once CUE has
// proper structural cycle detection. See Issue #42. Currently errors
- // occuring at a depth > 20 will not be detected.
- if x.depth > 20 {
+ // occuring at a depth > internal.MaxDepth will not be detected.
+ if x.depth > internal.MaxDepth {
return
}
ctx := v.ctx()
diff --git a/cue/validate.go b/cue/validate.go
index 5673524..08d17f0 100644
--- a/cue/validate.go
+++ b/cue/validate.go
@@ -14,6 +14,8 @@
package cue
+import "cuelang.org/go/internal"
+
// validate returns whether there is any error, recursively.
func validate(ctx *context, v value) (err *bottom) {
eval := v.evalPartial(ctx)
@@ -26,7 +28,7 @@
if err != nil {
return err
}
- if ctx.maxDepth++; ctx.maxDepth > 20 {
+ if ctx.maxDepth++; ctx.maxDepth > internal.MaxDepth {
return nil
}
for i, a := range x.arcs {
diff --git a/internal/diff/diff.go b/internal/diff/diff.go
index e3a5013..6c8689a 100644
--- a/internal/diff/diff.go
+++ b/internal/diff/diff.go
@@ -44,7 +44,7 @@
// Diff returns an edit script representing the difference between x and y.
func (p *Profile) Diff(x, y cue.Value) (Kind, *EditScript) {
- d := differ{cfg: p}
+ d := differ{cfg: *p}
return d.diffValue(x, y)
}
@@ -157,7 +157,7 @@
func (e Edit) YPos() int { return int(e.yPos - 1) }
type differ struct {
- cfg *Profile
+ cfg Profile
options []cue.Option
errs errors.Error
}
@@ -186,6 +186,11 @@
fallthrough
default:
+ // In concrete mode we do not care about non-concrete values.
+ if d.cfg.Concrete {
+ return Identity, nil
+ }
+
if !x.Equals(y) {
return Modified, nil
}
diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go
index e4b7a47..c52a2a1 100644
--- a/internal/diff/diff_test.go
+++ b/internal/diff/diff_test.go
@@ -23,10 +23,11 @@
func TestDiff(t *testing.T) {
testCases := []struct {
- name string
- x, y string
- kind Kind
- diff string
+ name string
+ x, y string
+ kind Kind
+ diff string
+ profile *Profile
}{{
name: "identity struct",
x: `{
@@ -302,6 +303,69 @@
- }
}
`,
+ }, {
+ x: `
+ Directory :: {
+ {
+ // Directory from another directory (e.g. subdirectory)
+ from: Directory
+ } | {
+ // Reference to remote directory
+ ref: string
+ } | {
+ // Use a local directory
+ local: string
+ }
+ path: string | *"/"
+ }
+ `,
+ y: `
+ Directory :: {
+ {
+ // Directory from another directory (e.g. subdirectory)
+ from: Directory
+ } | {
+ // Reference to remote directory
+ ref: string
+ } | {
+ // Use a local directory
+ local: string
+ }
+ path: string | *"/"
+ }
+ `,
+ profile: Final,
+ }, {
+ x: `
+ Directory :: {
+ {
+ // Directory from another directory (e.g. subdirectory)
+ from: Directory
+ } | {
+ // Reference to remote directory
+ ref: string
+ } | {
+ // Use a local directory
+ local: string
+ }
+ path: string | *"/"
+ }
+ `,
+ y: `
+ Directory :: {
+ {
+ // Directory from another directory (e.g. subdirectory)
+ from: Directory
+ } | {
+ // Reference to remote directory
+ ref: string
+ } | {
+ // Use a local directory
+ local: string
+ }
+ path: string | *"/"
+ }
+ `,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -314,7 +378,11 @@
if err != nil {
t.Fatal(err)
}
- kind, script := Diff(x.Value(), y.Value())
+ p := tc.profile
+ if p == nil {
+ p = Schema
+ }
+ kind, script := p.Diff(x.Value(), y.Value())
if kind != tc.kind {
t.Fatalf("got %d; want %d", kind, tc.kind)
}
diff --git a/internal/internal.go b/internal/internal.go
index cdf28dc..f12f267 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -206,3 +206,12 @@
func (e *decorated) Is(err error) bool {
return xerrors.Is(e.info, err) || xerrors.Is(e.cueError, err)
}
+
+// MaxDepth indicates the maximum evaluation depth. This is there to break
+// cycles in the absence of cycle detection.
+//
+// It is registered in a central place to make it easy to find all spots where
+// cycles are broken in this brute-force manner.
+//
+// TODO(eval): have cycle detection.
+const MaxDepth = 20