cue: allow cyclic references in lists
This is done by making lists considerably less efficient,
but that is for later concern. Still better, probably,
than creating a linked list.
Errors are now retained at the value level within lists.
This is okay, as validation descends into list values
as well.
Change-Id: Ifcd35d383f061dab82164d8da36f693dbea64847
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1900
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/ast.go b/cue/ast.go
index 5246b30..6d6e8fa 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -421,10 +421,12 @@
}
case *ast.ListLit:
- list := &list{baseValue: newExpr(n)}
- for _, e := range n.Elts {
- list.a = append(list.a, v.walk(e))
+ arcs := []arc{}
+ for i, e := range n.Elts {
+ arcs = append(arcs, arc{feature: label(i), v: v.walk(e)})
}
+ s := &structLit{baseValue: newExpr(n), arcs: arcs}
+ list := &list{baseValue: newExpr(n), elem: s}
list.initLit()
if n.Ellipsis != token.NoPos || n.Type != nil {
list.len = &bound{list.baseValue, opGeq, list.len}
diff --git a/cue/binop.go b/cue/binop.go
index 2cbd9aa..82bb1c9 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -926,22 +926,20 @@
src = mkBin(ctx, src.Pos(), op, x, other)
return ctx.mkErr(src, "incompatible list lengths: %v", n)
}
- var a, rest []value
- var rtyp value
- nx, ny := len(x.a), len(y.a)
- if nx < ny {
- a = make([]value, nx, ny)
- rest = y.a[nx:]
- rtyp = x.typ
-
- } else {
- a = make([]value, ny, nx)
- rest = x.a[ny:]
- rtyp = y.typ
+ sx := x.elem.arcs
+ xa := sx
+ sy := y.elem.arcs
+ ya := sy
+ for len(xa) < len(ya) {
+ xa = append(xa, arc{feature: label(len(xa)), v: x.typ})
}
+ for len(ya) < len(xa) {
+ ya = append(ya, arc{feature: label(len(ya)), v: y.typ})
+ }
+
typ := x.typ
max, ok := n.(*numLit)
- if !ok || len(a)+len(rest) < max.intValue(ctx) {
+ if !ok || len(xa) < max.intValue(ctx) {
typ = unify(ctx, src, x.typ.(evaluated), y.typ.(evaluated))
if isBottom(typ) {
src = mkBin(ctx, src.Pos(), op, x, other)
@@ -949,31 +947,25 @@
}
}
- for i := range a {
- ai := unify(ctx, src, x.at(ctx, i).evalPartial(ctx), y.at(ctx, i).evalPartial(ctx))
- if isBottom(ai) {
- return ai
- }
- a[i] = ai
- }
- for _, n := range rest {
- an := unify(ctx, src, n.evalPartial(ctx), rtyp.(evaluated))
- if isBottom(an) {
- return an
- }
- a = append(a, an)
- }
- return &list{baseValue: binSrc(src.Pos(), op, x, other), a: a, typ: typ, len: n}
+ // TODO: use forwarding instead of this mild hack.
+ x.elem.arcs = xa
+ y.elem.arcs = ya
+ s := unify(ctx, src, x.elem, y.elem).(*structLit)
+ x.elem.arcs = sx
+ y.elem.arcs = sy
+
+ base := binSrc(src.Pos(), op, x, other)
+ return &list{baseValue: base, elem: s, typ: typ, len: n}
case opEql, opNeq:
y, ok := other.(*list)
if !ok {
break
}
- if len(x.a) != len(y.a) {
+ if len(x.elem.arcs) != len(y.elem.arcs) {
return boolTonode(src, false)
}
- for i := range x.a {
+ for i := range x.elem.arcs {
if !test(ctx, src, op, x.at(ctx, i), y.at(ctx, i)) {
return boolTonode(src, false)
}
@@ -986,17 +978,24 @@
break
}
n := &list{baseValue: binSrc(src.Pos(), op, x, other), typ: y.typ}
- n.a = append(x.a, y.a...)
+ arcs := []arc{}
+ for _, v := range x.elem.arcs {
+ arcs = append(arcs, arc{feature: label(len(arcs)), v: v.v})
+ }
+ for _, v := range y.elem.arcs {
+ arcs = append(arcs, arc{feature: label(len(arcs)), v: v.v})
+ }
switch v := y.len.(type) {
case *numLit:
// Closed list
ln := &numLit{numBase: v.numBase}
- ln.v.SetInt64(int64(len(n.a)))
+ ln.v.SetInt64(int64(len(arcs)))
n.len = ln
default:
// Open list
- n.len = y.len
+ n.len = y.len // TODO: add length of x?
}
+ n.elem = &structLit{baseValue: n.baseValue, arcs: arcs}
return n
case opMul:
@@ -1005,7 +1004,8 @@
panic("multiplication must be int type")
}
n := &list{baseValue: binSrc(src.Pos(), op, x, other), typ: x.typ}
- if len(x.a) > 0 {
+ arcs := []arc{}
+ if len(x.elem.arcs) > 0 {
if !k.isGround() {
// should never reach here.
break
@@ -1013,7 +1013,9 @@
if ln := other.(*numLit).intValue(ctx); ln > 0 {
for i := 0; i < ln; i++ {
// TODO: copy values
- n.a = append(n.a, x.a...)
+ for _, a := range x.elem.arcs {
+ arcs = append(arcs, arc{feature: label(len(arcs)), v: a.v})
+ }
}
} else if ln < 0 {
return ctx.mkErr(src, "negative number %d multiplies list", ln)
@@ -1023,12 +1025,13 @@
case *numLit:
// Closed list
ln := &numLit{numBase: v.numBase}
- ln.v.SetInt64(int64(len(n.a)))
+ ln.v.SetInt64(int64(len(arcs)))
n.len = ln
default:
// Open list
- n.len = x.len
+ n.len = x.len // TODO: multiply length?
}
+ n.elem = &structLit{baseValue: n.baseValue, arcs: arcs}
return n
}
return ctx.mkIncompatible(src, op, x, other)
diff --git a/cue/debug.go b/cue/debug.go
index 136379d..718040a 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -363,13 +363,13 @@
case *top, *basicType:
open = true
}
- if !ok || ln > len(x.a) {
+ if !ok || ln > len(x.elem.arcs) {
if !open && !isTop(x.typ) {
p.debugStr(x.len)
write("*[")
p.debugStr(x.typ)
write("]")
- if len(x.a) == 0 {
+ if len(x.elem.arcs) == 0 {
break
}
write("(")
@@ -378,9 +378,9 @@
ellipsis = true
}
write("[")
- for i, a := range x.a {
- p.debugStr(a)
- if i < len(x.a)-1 {
+ for i, a := range x.elem.arcs {
+ p.debugStr(a.v)
+ if i < len(x.elem.arcs)-1 {
write(",")
}
}
diff --git a/cue/eval.go b/cue/eval.go
index b908013..f627cee 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -219,34 +219,24 @@
if err := firstBottom(n, t); err != nil {
return err
}
- a := make([]value, len(x.a))
- changed := false
- for i, v := range x.a {
- // TODO: don't evaluate now. List elements may refer to other list
- // elements. Evaluating them here will cause a cycle evaluating the
- // struct field.
- e := v.evalPartial(ctx)
- changed = changed || e != v
- switch e.(type) {
- case *bottom:
- return e
- case value:
- a[i] = e
- }
- }
- if !changed && n == x.len && t == x.typ {
+ s := x.elem.evalPartial(ctx).(*structLit)
+ if s == x.elem && n == x.len && t == x.typ {
return x
}
- return &list{x.baseValue, a, t, n}
+ return &list{x.baseValue, s, t, n}
}
func (x *listComprehension) evalPartial(ctx *context) evaluated {
- list := &list{baseValue: x.baseValue}
+ s := &structLit{baseValue: x.baseValue}
+ list := &list{baseValue: x.baseValue, elem: s}
result := x.clauses.yield(ctx, func(k, v evaluated, _ bool) *bottom {
if !k.kind().isAnyOf(intKind) {
return ctx.mkErr(k, "key must be of type int")
}
- list.a = append(list.a, v.evalPartial(ctx))
+ list.elem.arcs = append(list.elem.arcs, arc{
+ feature: label(len(list.elem.arcs)),
+ v: v.evalPartial(ctx),
+ })
return nil
})
switch {
diff --git a/cue/export.go b/cue/export.go
index cd4e63c..a61131a 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -411,8 +411,12 @@
case *list:
list := &ast.ListLit{}
var expr ast.Expr = list
- for _, e := range x.a {
- list.Elts = append(list.Elts, p.expr(e))
+ for _, e := range x.elem.arcs {
+ if doEval(p.mode) {
+ list.Elts = append(list.Elts, p.expr(e.v.evalPartial(p.ctx)))
+ } else {
+ list.Elts = append(list.Elts, p.expr(e.v))
+ }
}
max := maxNum(x.len)
num, ok := max.(*numLit)
@@ -430,7 +434,7 @@
case *top, *basicType:
open = true
}
- if !ok || ln > len(x.a) {
+ if !ok || ln > len(x.elem.arcs) {
list.Type = p.expr(x.typ)
if !open && !isTop(x.typ) {
expr = &ast.BinaryExpr{
diff --git a/cue/go.go b/cue/go.go
index 491571a..c452228 100644
--- a/cue/go.go
+++ b/cue/go.go
@@ -346,13 +346,15 @@
case reflect.Slice, reflect.Array:
list := &list{baseValue: src.base()}
+ arcs := []arc{}
for i := 0; i < value.Len(); i++ {
x := convert(ctx, src, value.Index(i).Interface())
if isBottom(x) {
return x
}
- list.a = append(list.a, x)
+ arcs = append(arcs, arc{feature: label(len(arcs)), v: x})
}
+ list.elem = &structLit{baseValue: list.baseValue, arcs: arcs}
list.initLit()
// There is no need to set the type of the list, as the list will
// be of fixed size and all elements will already have a defined
@@ -522,7 +524,7 @@
if t.Kind() == reflect.Array {
ln = toInt(ctx, baseValue{}, int64(t.Len()))
}
- e = &list{typ: elem, len: ln}
+ e = &list{elem: &structLit{}, typ: elem, len: ln}
}
if k == reflect.Slice {
e = wrapOrNull(e)
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 7b50370..1e58e59 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -337,7 +337,7 @@
e4: [1, 2, ...>=4 & <=5] & [1, 2, 4, 8]
e5: [1, 2, 4, 8] & [1, 2, ...>=4 & <=5]
`,
- out: `<0>{list: [1,2,3], index: 2, unify: [1,2,3], e: _|_(([] & 4):unsupported op &(list, number)), e2: _|_("d":invalid list index "d" (type string)), e3: _|_(-1:invalid list index -1 (index must be non-negative)), e4: _|_((<=5 & 8):8 not within bound <=5), e5: _|_((<=5 & 8):8 not within bound <=5)}`,
+ out: `<0>{list: [1,2,3], index: 2, unify: [1,2,3], e: _|_(([] & 4):unsupported op &(list, number)), e2: _|_("d":invalid list index "d" (type string)), e3: _|_(-1:invalid list index -1 (index must be non-negative)), e4: [1,2,4,_|_((<=5 & 8):8 not within bound <=5)], e5: [1,2,4,_|_((<=5 & 8):8 not within bound <=5)]}`,
}, {
desc: "list arithmetic",
in: `
@@ -473,7 +473,7 @@
c: [c[1], c[0]]
`,
- out: `<0>{a: _|_(cycle detected), b: _|_(cycle detected), c: _|_(cycle detected)}`,
+ out: `<0>{a: _|_(cycle detected), b: _|_(cycle detected), c: [_|_(cycle detected),_|_(cycle detected)]}`,
}, {
desc: "resolved self-reference cycles",
in: `
@@ -481,13 +481,13 @@
b: a + 100
b: 200
- c: [c[1], a] // TODO: should be allowed
+ c: [c[1], a]
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}}`,
+ out: `<0>{a: 100, b: 200, c: [100,100], 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: "resolved self-reference cycles: Issue 19",
in: `
@@ -1029,6 +1029,16 @@
`feq0: false, feq1: false, feq2: false, feq3: false, feq4: false, feq5: false, feq6: false, feq7: false, feq8: false, feq9: false, feq10: false, feq11: false, ` +
`fne0: false, fne1: false, fne2: false, fne3: false, fne4: false, fne5: false, fne6: false, fne7: false, fne8: false, fne9: false, fne10: false, fne11: false}`,
}, {
+ desc: "list unification",
+ in: `
+ a: { l: ["foo", v], v: l[1] }
+ b: a & { l: [_, "bar"] }
+ `,
+ out: `<0>{` +
+ `a: <1>{l: ["foo",_|_(cycle detected)], ` +
+ `v: _|_(cycle detected)}, ` +
+ `b: <2>{l: ["foo","bar"], v: "bar"}}`,
+ }, {
desc: "correct error messages",
// Tests that it is okay to partially evaluate structs.
in: `
diff --git a/cue/rewrite.go b/cue/rewrite.go
index fe7faeb..9515d3c 100644
--- a/cue/rewrite.go
+++ b/cue/rewrite.go
@@ -105,18 +105,13 @@
}
func (x *list) rewrite(ctx *context, fn rewriteFunc) value {
- a := make([]value, len(x.a))
- changed := false
- for i, e := range x.a {
- a[i] = rewrite(ctx, e, fn)
- changed = changed || a[i] != e
- }
+ elem := rewrite(ctx, x.elem, fn).(*structLit)
typ := rewrite(ctx, x.typ, fn)
len := rewrite(ctx, x.len, fn)
- if !changed && typ == x.typ && len == x.len {
+ if elem == x.elem && typ == x.typ && len == x.len {
return x
}
- return &list{x.baseValue, a, typ, len}
+ return &list{x.baseValue, elem, typ, len}
}
func (x *sliceExpr) rewrite(ctx *context, fn rewriteFunc) value {
diff --git a/cue/rewrite_test.go b/cue/rewrite_test.go
index 5180509..adf3476 100644
--- a/cue/rewrite_test.go
+++ b/cue/rewrite_test.go
@@ -93,13 +93,10 @@
obj := &structLit{x.baseValue, emit, t, nil, arcs, nil}
return obj
case *list:
- a := make([]value, len(x.a))
- for i := range x.a {
- a[i] = rewriteRec(ctx, x.a[i], x.at(ctx, i), m)
- }
+ elm := rewriteRec(ctx, x.elem, x.elem, m).(*structLit)
len := rewriteRec(ctx, x.len, x.len.(evaluated), m)
typ := rewriteRec(ctx, x.typ, x.typ.(evaluated), m)
- return &list{x.baseValue, a, typ, len}
+ return &list{x.baseValue, elm, typ, len}
default:
return eval
}
diff --git a/cue/subsume.go b/cue/subsume.go
index 29df434..ffd70bf 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -239,20 +239,19 @@
if !subsumes(ctx, x.len, y.len, mode) {
return false
}
- n := len(x.a)
- if len(y.a) < n {
- n = len(y.a)
+ if !subsumes(ctx, x.elem, y.elem, mode) {
+ return false
}
- for i, a := range x.a[:n] {
- if !subsumes(ctx, a, y.a[i], mode) {
- return false
- }
+ n := len(x.elem.arcs)
+ if len(y.elem.arcs) < n {
+ n = len(y.elem.arcs)
}
if y.isOpen() {
return subsumes(ctx, x.typ, y.typ, 0)
}
- for i := range y.a[n:] {
- if !subsumes(ctx, x.typ, y.a[i], mode) {
+ for _, a := range y.elem.arcs[n:] {
+ // TODO: evaluate?
+ if !subsumes(ctx, x.typ, a.v, mode) {
return false
}
}
diff --git a/cue/types.go b/cue/types.go
index 5184904..3bf6637 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -568,7 +568,7 @@
return json.Marshal(x.(*bytesLit).b)
case listKind:
l := x.(*list)
- i := Iterator{ctx: ctx, val: v, iter: l, len: len(l.a)}
+ i := Iterator{ctx: ctx, val: v, iter: l, len: len(l.elem.arcs)}
return marshalList(&i)
case structKind:
obj, _ := v.structVal(ctx)
@@ -730,7 +730,7 @@
return Iterator{ctx: ctx}, err
}
l := v.eval(ctx).(*list)
- return Iterator{ctx: ctx, val: v, iter: l, len: len(l.a)}, nil
+ return Iterator{ctx: ctx, val: v, iter: l, len: len(l.elem.arcs)}, nil
}
// Null reports an error if v is not null.
@@ -1096,7 +1096,7 @@
func isGroundRecursive(ctx *context, v value) error {
switch x := v.(type) {
case *list:
- for i := 0; i < len(x.a); i++ {
+ for i := 0; i < len(x.elem.arcs); i++ {
v := ctx.manifest(x.at(ctx, i))
if err := isGroundRecursive(ctx, v); err != nil {
return err
diff --git a/cue/validate.go b/cue/validate.go
index 7e17216..0379d71 100644
--- a/cue/validate.go
+++ b/cue/validate.go
@@ -29,11 +29,8 @@
}
}
case *list:
- for _, v := range x.a {
- if err := validate(ctx, v); err != nil {
- return err
- }
- }
+ // TODO: also validate types for open lists?
+ return validate(ctx, x.elem)
}
return nil
}
diff --git a/cue/value.go b/cue/value.go
index d1d8cf6..58a7738 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -438,8 +438,7 @@
type list struct {
baseValue
- // TODO: Elements in a list are nodes to allow for cycle detection.
- a []value // TODO: could be arc?
+ elem *structLit
typ value
@@ -451,7 +450,7 @@
// initLit initializes a literal list.
func (x *list) initLit() {
n := newNum(x, intKind)
- n.v.SetInt64(int64(len(x.a)))
+ n.v.SetInt64(int64(len(x.elem.arcs)))
x.len = n
x.typ = &top{x.baseValue}
}
@@ -481,8 +480,10 @@
v := ctx.mkErr(x, "index %d out of bounds", i)
return arc{cache: v}
}
- if i < len(x.a) {
- return arc{cache: x.a[i].evalPartial(ctx), v: x.a[i]}
+ if i < len(x.elem.arcs) {
+ a := x.elem.iterAt(ctx, i)
+ a.feature = 0
+ return a
}
max := maxNum(x.len.(evaluated))
if max.kind().isGround() {
@@ -504,7 +505,7 @@
// lo and hi must be nil or a ground integer.
func (x *list) slice(ctx *context, lo, hi *numLit) evaluated {
- a := x.a
+ a := x.elem.arcs
max := maxNum(x.len).evalPartial(ctx)
if hi != nil {
n := hi.intValue(ctx)
@@ -535,10 +536,15 @@
if n < len(a) {
a = a[n:]
} else {
- a = []value{}
+ a = []arc{}
}
}
- return &list{baseValue: x.baseValue, a: a, typ: x.typ, len: max}
+ arcs := make([]arc, len(a))
+ for i, a := range a {
+ arcs[i] = arc{feature: label(i), v: a.v}
+ }
+ s := &structLit{baseValue: x.baseValue, arcs: arcs}
+ return &list{baseValue: x.baseValue, elem: s, typ: x.typ, len: max}
}
// An structLit is a single structLit in the configuration tree.
@@ -1205,7 +1211,7 @@
return nil
case *list:
- for i := range src.a {
+ for i := range src.elem.arcs {
idx := newNum(x, intKind)
idx.v.SetInt64(int64(i))
v := fn.call(ctx, x, idx, src.at(ctx, i))