cue: map API values to more native ADT
Before, Unify would add a Vertex as conjuncts and let
the evaluator treat this as a special case. The result was
not entirely correct and hard to fix. WIth this change, Unify
maps directly to a correct ADT by introduce a "phantom"
parent with the correct closed struct.
Fixes #567
Fixes #563
Fixes #561
Change-Id: I11d0e345772695d5ed6fcfd7c6b9e0f2cb2110a4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7746
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Paul Jolly <paul@myitcv.org.uk>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/types.go b/cue/types.go
index e8f97f2..f5570f5 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -1636,13 +1636,10 @@
var value = convert.GoValueToValue(ctx.opCtx, x, true)
n, _ := value.(*adt.Vertex)
if n == nil {
- n = &adt.Vertex{Label: v.v.Label}
+ n = &adt.Vertex{}
n.AddConjunct(adt.MakeRootConjunct(nil, value))
- } else {
- n.Label = v.v.Label
}
n.Finalize(ctx.opCtx)
- n.Parent = v.v.Parent
w := makeValue(v.idx, n)
return v.Unify(w)
}
@@ -1728,23 +1725,21 @@
// Value v and w must be obtained from the same build.
// TODO: remove this requirement.
func (v Value) Unify(w Value) Value {
- // ctx := v.ctx()
if v.v == nil {
return w
}
if w.v == nil {
return v
}
- n := &adt.Vertex{Label: v.v.Label}
- n.AddConjunct(adt.MakeRootConjunct(nil, v.v))
- n.AddConjunct(adt.MakeRootConjunct(nil, w.v))
+
+ n := &adt.Vertex{}
+
+ eval.AddVertex(n, v.v)
+ eval.AddVertex(n, w.v)
ctx := v.idx.newContext()
n.Finalize(ctx.opCtx)
- // We do not set the parent until here as we don't want to "inherit" the
- // closedness setting from v.
- n.Parent = v.v.Parent
return makeValue(v.idx, n)
}
@@ -1761,6 +1756,12 @@
panic("accept must exist")
}
+ // TODO: take a similar approach for UnifyAccept as for Unify. In this
+ // case though, the fields should be added as an embeding.
+ //
+ // n := &adt.Vertex{}
+ // eval.EmbedVertex(n, v.v)
+ // eval.EmbedVertex(n, w.v)
n := &adt.Vertex{Parent: v.v.Parent, Label: v.v.Label}
n.AddConjunct(adt.MakeRootConjunct(nil, v.v))
n.AddConjunct(adt.MakeRootConjunct(nil, w.v))
@@ -1782,7 +1783,6 @@
return false
}
return eval.Equal(v.ctx().opCtx, v.v, other.v)
-
}
// Format prints a debug version of a value.
diff --git a/cue/types_test.go b/cue/types_test.go
index 5bda591..bc08bd0 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -44,6 +44,60 @@
return insts[0]
}
+func TestAPI(t *testing.T) {
+ testCases := []struct {
+ input string
+ fun func(i *Instance) Value
+ want string
+ skip bool
+ }{{
+ // Issue #567
+ input: `
+ #runSpec: {action: foo: int}
+
+ v: {ction: foo: 1}
+ `,
+ fun: func(i *Instance) Value {
+ runSpec := i.LookupDef("#runSpec")
+ v := i.Lookup("v")
+ res := runSpec.Unify(v)
+ return res
+ },
+ want: "_|_(#runSpec: field `ction` not allowed)",
+ }, {
+ // Issue #567
+ input: `
+ #runSpec: {action: foo: int}
+
+ v: {action: Foo: 1}
+ `,
+ fun: func(i *Instance) Value {
+ runSpec := i.LookupDef("#runSpec")
+ v := i.Lookup("v")
+ res := runSpec.Unify(v)
+ return res
+ },
+ want: "_|_(#runSpec.action: field `Foo` not allowed)",
+ }}
+ for _, tc := range testCases {
+ if tc.skip {
+ continue
+ }
+ t.Run("", func(t *testing.T) {
+ var r Runtime
+ inst, err := r.Compile("in", tc.input)
+ if err != nil {
+ t.Fatal(err)
+ }
+ v := tc.fun(inst)
+ got := fmt.Sprintf("%+v", v)
+ if got != tc.want {
+ t.Errorf("got:\n%s\nwant:\n%s", got, tc.want)
+ }
+ })
+ }
+}
+
func TestValueType(t *testing.T) {
testCases := []struct {
value string
@@ -2574,7 +2628,6 @@
}
}
-// TODO: stack overflow
func TestPathCorrection(t *testing.T) {
testCases := []struct {
input string
diff --git a/internal/core/eval/closed.go b/internal/core/eval/closed.go
index 8fbd14d..3491dfa 100644
--- a/internal/core/eval/closed.go
+++ b/internal/core/eval/closed.go
@@ -70,11 +70,17 @@
Canopy []CloseDef
Fields []*fieldSet
- // TODO: remove (unused as not fine-grained enough)
- // isClosed is now used as an approximate filter.
+ // TODO: isClosed could be removed if we can include closedness fully
+ // in the ClosedDefs representing conjuncts. This, in turn, will allow
+ // various optimizations, like having to record field sets for data
+ // vertices.
isClosed bool
isList bool
openList bool
+
+ // ignore tells to the closedness check for one level. This allows
+ // constructed parent nodes that do not have field sets defined.
+ ignore bool
}
func (a *acceptor) clone() *acceptor {
@@ -191,6 +197,30 @@
type Entry = fieldSet
+// TODO: this may be an idea to get rid of acceptor.isClosed. There
+// are various more things to consider, though.
+//
+// func (c *acceptor) isRequired(id adt.ID) bool {
+// req := false
+// c.visitAnd(id, func(id adt.ID, n CloseDef) bool {
+// if n.isRequired() {
+// req = true
+// return false
+// }
+// c.visitEmbed(id, func(id adt.ID, n CloseDef) bool {
+// if n.isRequired() {
+// req = true
+// return false
+// }
+// req = req || c.isRequired(id)
+// return true
+// })
+
+// return true
+// })
+// return req
+// }
+
func (c *acceptor) visitAllFieldSets(f func(f *fieldSet)) {
for _, set := range c.Fields {
for ; set != nil; set = set.next {
@@ -313,7 +343,7 @@
func isComplexStruct(v *adt.Vertex) bool {
m, _ := v.Value.(*adt.StructMarker)
if m == nil {
- return true
+ return false
}
a, _ := v.Closed.(*acceptor)
if a == nil {
@@ -334,10 +364,15 @@
}
// InsertSubtree inserts the closedness information of v into c as an embedding
-// at the current position and inserts conjuncts of v into n (if not nil).
-// It inserts it as an embedding and not and to cover either case. The idea is
-// that one of the values were supposed to be closed, a separate node entry
-// would already have been created.
+// at the current position and inserts conjuncts of v into n (if not nil). It
+// inserts it as an embedding and not and to cover either case. The idea is that
+// one of the values were supposed to be closed, a separate node entry would
+// already have been created.
+//
+// TODO: get rid of this. This is now only used in newDisjunctionAcceptor,
+// which, in turn, is rarely used for analyzing disjunction values in the API.
+// This code is not ideal and buggy (see comment below), but it doesn't seem
+// worth improving it and we can probably do without.
func (c *acceptor) InsertSubtree(at adt.ID, n *nodeContext, v *adt.Vertex, cyclic bool) adt.ID {
if len(c.Canopy) == 0 {
c.Canopy = append(c.Canopy, CloseDef{})
@@ -346,6 +381,15 @@
panic(fmt.Sprintf("at >= len(canopy) (%d >= %d)", at, len(c.Canopy)))
}
+ // TODO: like with AddVertex, this really should use the acceptor of the
+ // parent. This seems not to work, though.
+ //
+ // var a *acceptor
+ // if v.Parent != nil && v.Parent.Closed != nil {
+ // a = closedInfo(v.Parent)
+ // } else {
+ // a = &acceptor{}
+ // }
a := closedInfo(v)
a.node(0)
@@ -363,12 +407,13 @@
c.Canopy = append(c.Canopy, a.Canopy...)
// Shift all IDs for the new offset.
- for i, x := range c.Canopy[id:] {
+ for i := int(id); i < len(c.Canopy); i++ {
+ x := c.Canopy[i]
if x.And != -1 {
- c.Canopy[int(id)+i].And += id
+ c.Canopy[i].And += id
}
if x.NextEmbed != 0 {
- c.Canopy[int(id)+i].NextEmbed += id
+ c.Canopy[i].NextEmbed += id
}
}
@@ -383,6 +428,187 @@
return id
}
+func appendConjuncts(v *adt.Vertex, a []adt.Conjunct) {
+ for _, c := range a {
+ v.AddConjunct(c)
+ }
+}
+
+// AddVertex add a Vertex to a new destination node. The caller may
+// call AddVertex multiple times on dst. None of the fields of dst
+// should be set by the caller. AddVertex takes care of setting the
+// Label and Parent.
+func AddVertex(dst, src *adt.Vertex) {
+ if dst.Parent == nil {
+ // Create "fake" parent that holds the combined closed data.
+ // We do not set the parent until here as we don't want to "inherit" the
+ // closedness setting from v.
+ dst.Parent = &adt.Vertex{Parent: src.Parent}
+ dst.Label = src.Label
+ }
+
+ if src.IsData() {
+ dst.AddConjunct(adt.MakeConjunct(nil, src, 0))
+ return
+ }
+
+ var srcC *acceptor
+ if src.Parent != nil && src.Parent.Closed != nil {
+ srcC = closedInfo(src.Parent)
+ } else {
+ srcC = &acceptor{}
+ }
+ dstC := closedInfo(dst.Parent)
+ dstC.ignore = true
+
+ isDef := src.Label.IsDef()
+ addClose := isDef || srcC.isClosed
+
+ if addClose {
+ dstC.node(0)
+ } else if len(dstC.Canopy) == 0 {
+ // we can copy it as is (assuming 0 is )
+ appendConjuncts(dst, src.Conjuncts)
+ dstC.Canopy = append(dstC.Canopy, srcC.Canopy...)
+ return
+ }
+
+ offset := adt.ID(len(dstC.Canopy))
+ top := offset
+
+ switch {
+ case len(srcC.Canopy) == 0 && !addClose:
+ appendConjuncts(dst, src.Conjuncts)
+ return
+
+ case len(srcC.Canopy) == 1:
+ c := srcC.Canopy[0]
+ if !addClose && !c.IsDef && !c.IsClosed {
+ appendConjuncts(dst, src.Conjuncts)
+ return
+ }
+ c.IsDef = addClose
+ dstC.Canopy = append(dstC.Canopy, c)
+
+ case addClose:
+ srcC.node(0)
+ isClosed := false
+ srcC.visitAnd(0, func(id adt.ID, c CloseDef) bool {
+ if c.IsClosed || c.IsDef {
+ isClosed = true
+ return false
+ }
+ return true
+ })
+
+ if !isClosed {
+ // need to embed and close.
+ dstC.Canopy = append(dstC.Canopy,
+ CloseDef{
+ And: offset,
+ IsDef: true,
+ NextEmbed: offset + 1,
+ },
+ CloseDef{
+ And: embedRoot,
+ })
+
+ offset += 2
+ }
+ fallthrough
+
+ default:
+ dstC.Canopy = append(dstC.Canopy, srcC.Canopy...)
+ }
+
+ // Shift all IDs for the new offset.
+ for i := int(offset); i < len(dstC.Canopy); i++ {
+ x := dstC.Canopy[i]
+ if x.And != -1 {
+ dstC.Canopy[i].And += offset
+ }
+ if x.NextEmbed != 0 {
+ dstC.Canopy[i].NextEmbed += offset
+ }
+ }
+
+ topAnd := dstC.Canopy[top].And
+ atAnd := dstC.Canopy[0].And
+ dstC.Canopy[top].And = atAnd
+ dstC.Canopy[0].And = topAnd
+
+ for _, c := range src.Conjuncts {
+ c.CloseID += offset
+ if err := dst.AddConjunct(c); err != nil {
+ panic(err)
+ }
+ }
+}
+
+// TODO: This is roughly the equivalent of AddVertex for use with
+// UnifyAccept. It is based on the old InsertSubTree. We should use
+// this at some point, but ideally we should have a better way to
+// represent closedness in the first place that is more flexible in
+// handling API usage.
+//
+// func EmbedVertex(dst, src *adt.Vertex) {
+// if dst.Parent == nil {
+// // Create "fake" parent that holds the combined closed data.
+// // We do not set the parent until here as we don't want to "inherit" the
+// // closedness setting from v.
+// dst.Parent = &adt.Vertex{Parent: src.Parent}
+// dst.Label = src.Label
+// }
+//
+// if src.IsData() {
+// dst.AddConjunct(adt.MakeConjunct(nil, src, 0))
+// return
+// }
+//
+// var a *acceptor
+// if src.Parent != nil && src.Parent.Closed != nil {
+// a = closedInfo(src.Parent)
+// } else {
+// a = &acceptor{}
+// }
+// a.node(0)
+//
+// c := closedInfo(dst.Parent)
+// c.ignore = true
+// c.node(0)
+//
+// id := adt.ID(len(c.Canopy))
+// y := CloseDef{
+// And: embedRoot,
+// NextEmbed: c.Canopy[0].NextEmbed,
+// }
+// c.Canopy[0].NextEmbed = id
+//
+// c.Canopy = append(c.Canopy, y)
+// id = adt.ID(len(c.Canopy))
+//
+// // First entry is at the embedded node location.
+// c.Canopy = append(c.Canopy, a.Canopy...)
+//
+// // Shift all IDs for the new offset.
+// for i := int(id); i < len(c.Canopy); i++ {
+// x := c.Canopy[i]
+// if x.And != -1 {
+// c.Canopy[i].And += id
+// }
+// if x.NextEmbed != 0 {
+// c.Canopy[i].NextEmbed += id
+// }
+// }
+//
+// for _, c := range src.Conjuncts {
+// c.CloseID += id
+// if err := dst.AddConjunct(c); err != nil {
+// panic(err)
+// }
+// }
+// }
+
func (c *acceptor) verifyArc(ctx *adt.OpContext, f adt.Feature, v *adt.Vertex) (found bool, err *adt.Bottom) {
defer ctx.ReleasePositions(ctx.MarkPositions())
diff --git a/internal/core/eval/eval.go b/internal/core/eval/eval.go
index db3e806..74e236a 100644
--- a/internal/core/eval/eval.go
+++ b/internal/core/eval/eval.go
@@ -312,7 +312,7 @@
}
v.Arcs = nil
// v.Structs = nil // TODO: should we keep or discard the Structs?
- v.Closed = newDisjunctionAcceptor(d)
+ v.Closed = newDisjunctionAcceptor(d) // TODO: remove?
default:
if r := n.result(); r.Value != nil {
@@ -341,7 +341,7 @@
closedInfo, _ = v.Parent.Closed.(*acceptor)
}
- if !v.Label.IsInt() && closedInfo != nil {
+ if !v.Label.IsInt() && closedInfo != nil && !closedInfo.ignore {
ci := closedInfo
// Visit arcs recursively to validate and compute error.
switch ok, err := ci.verifyArc(c, v.Label, v); {
@@ -355,7 +355,7 @@
return shared
case !ok: // hidden field
- // A hidden field is except from checking. Ensure that the
+ // A hidden field is exempt from checking. Ensure that the
// closedness doesn't carry over into children.
// TODO: make this more fine-grained per package, allowing
// checked restrictions to be defined within the package.
@@ -1086,6 +1086,13 @@
id := v.CloseID
switch x := v.Expr().(type) {
+ case *adt.Vertex:
+ if x.IsData() {
+ n.addValueConjunct(env, x, id)
+ } else {
+ n.addVertexConjuncts(env, id, x, x)
+ }
+
case adt.Value:
n.addValueConjunct(env, x, id)
@@ -1178,6 +1185,17 @@
}
}
+ // TODO: also to through normal Vertex handling here. At the moment
+ // addValueConjunct handles StructMarker.NeedsClose, as this is always
+ // only needed when evaluation an Evaluator, and not a Resolver.
+ // The two code paths should ideally be merged once this separate
+ // mechanism is eliminated.
+ //
+ // if arc, ok := val.(*adt.Vertex); ok && !arc.IsData() {
+ // n.addVertexConjuncts(v.Env, closeID, v.Expr(), arc)
+ // break
+ // }
+
// TODO: insert in vertex as well
n.addValueConjunct(v.Env, val, closeID)
@@ -1372,9 +1390,11 @@
cyclic := env != nil && env.Cyclic
- if !x.IsData() && len(x.Conjuncts) > 0 {
+ if !x.IsData() {
+ // TODO: this really shouldn't happen anymore.
if isComplexStruct(x) {
- closedInfo(n.node).InsertSubtree(id, n, x, cyclic)
+ // This really shouldn't happen, but just in case.
+ n.addVertexConjuncts(env, id, x, x)
return
}
diff --git a/internal/core/eval/vertex_test.go b/internal/core/eval/vertex_test.go
index 04dcb13..c86cf51 100644
--- a/internal/core/eval/vertex_test.go
+++ b/internal/core/eval/vertex_test.go
@@ -113,8 +113,8 @@
t.Run("", func(t *testing.T) {
n := &adt.Vertex{}
- n.AddConjunct(adt.MakeRootConjunct(nil, tc.a))
- n.AddConjunct(adt.MakeRootConjunct(nil, tc.b))
+ eval.AddVertex(n, tc.a)
+ eval.AddVertex(n, tc.b)
n.Finalize(ctx)
got := debug.NodeString(r, n, &debug.Config{Compact: !tc.verbose})
diff --git a/pkg/list/testdata/gen.txtar b/pkg/list/testdata/gen.txtar
index 01cbcb5..fc6635e 100644
--- a/pkg/list/testdata/gen.txtar
+++ b/pkg/list/testdata/gen.txtar
@@ -76,12 +76,12 @@
error in call to list.Slice: slice bounds out of range
error in call to list.Take: negative index
0: error in call to list.SortStrings: element 0 of list argument 0: cannot use value 1 (type int) as string
-x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
-x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
-x: error in call to list.Sort: empty disjunction: 2 related errors:
-y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
-y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
-y: error in call to list.Sort: empty disjunction: 2 related errors:
+Ascending.x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
+Ascending.x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
+Ascending.x: error in call to list.Sort: empty disjunction: 2 related errors:
+Ascending.y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
+Ascending.y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
+Ascending.y: error in call to list.Sort: empty disjunction: 2 related errors:
t3: cannot use "foo" (type string) as list in argument 1 to list.Avg:
./in.cue:5:14
t14: cannot use "foo" (type string) as int in argument 2 to list.FlattenN:
@@ -270,12 +270,12 @@
}
}
t40: (_|_){
- // [eval] x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
- // x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
- // x: error in call to list.Sort: empty disjunction: 2 related errors:
- // y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
- // y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
- // y: error in call to list.Sort: empty disjunction: 2 related errors:
+ // [eval] Ascending.x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
+ // Ascending.x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
+ // Ascending.x: error in call to list.Sort: empty disjunction: 2 related errors:
+ // Ascending.y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
+ // Ascending.y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
+ // Ascending.y: error in call to list.Sort: empty disjunction: 2 related errors:
}
t41: (#list){
0: (string){ "a" }
@@ -310,11 +310,11 @@
t52: (bool){ true }
t53: (bool){ false }
t54: (_|_){
- // [eval] x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
- // x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
- // x: error in call to list.Sort: empty disjunction: 2 related errors:
- // y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
- // y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
- // y: error in call to list.Sort: empty disjunction: 2 related errors:
+ // [eval] Ascending.x: error in call to list.Sort: conflicting values number and {b:2} (mismatched types number and struct)
+ // Ascending.x: error in call to list.Sort: conflicting values string and {b:2} (mismatched types string and struct)
+ // Ascending.x: error in call to list.Sort: empty disjunction: 2 related errors:
+ // Ascending.y: error in call to list.Sort: conflicting values number and {a:1} (mismatched types number and struct)
+ // Ascending.y: error in call to list.Sort: conflicting values string and {a:1} (mismatched types string and struct)
+ // Ascending.y: error in call to list.Sort: empty disjunction: 2 related errors:
}
}
diff --git a/pkg/list/testdata/issues.txtar b/pkg/list/testdata/issues.txtar
new file mode 100644
index 0000000..5108373
--- /dev/null
+++ b/pkg/list/testdata/issues.txtar
@@ -0,0 +1,102 @@
+-- in.cue --
+import "list"
+
+issue563: {
+ #MyDef: {
+ name: string
+ ...
+ }
+
+ _all: [
+ _a,
+ _b,
+ ]
+
+ _a: [...#MyDef] & [
+ { name: "a" },
+ { name: "b" },
+ { name: "c" },
+ ]
+
+ _b: [...#MyDef] & [
+ { name: "1" },
+ { name: "2" },
+ { name: "3" },
+ ]
+
+ output: [...#MyDef] & list.FlattenN(_all, 1)
+}
+-- out/list --
+(struct){
+ issue563: (struct){
+ #MyDef: (#struct){
+ name: (string){ string }
+ }
+ _all: (#list){
+ 0: (#list){
+ 0: (#struct){
+ name: (string){ "a" }
+ }
+ 1: (#struct){
+ name: (string){ "b" }
+ }
+ 2: (#struct){
+ name: (string){ "c" }
+ }
+ }
+ 1: (#list){
+ 0: (#struct){
+ name: (string){ "1" }
+ }
+ 1: (#struct){
+ name: (string){ "2" }
+ }
+ 2: (#struct){
+ name: (string){ "3" }
+ }
+ }
+ }
+ _a: (#list){
+ 0: (#struct){
+ name: (string){ "a" }
+ }
+ 1: (#struct){
+ name: (string){ "b" }
+ }
+ 2: (#struct){
+ name: (string){ "c" }
+ }
+ }
+ _b: (#list){
+ 0: (#struct){
+ name: (string){ "1" }
+ }
+ 1: (#struct){
+ name: (string){ "2" }
+ }
+ 2: (#struct){
+ name: (string){ "3" }
+ }
+ }
+ output: (#list){
+ 0: (#struct){
+ name: (string){ "a" }
+ }
+ 1: (#struct){
+ name: (string){ "b" }
+ }
+ 2: (#struct){
+ name: (string){ "c" }
+ }
+ 3: (#struct){
+ name: (string){ "1" }
+ }
+ 4: (#struct){
+ name: (string){ "2" }
+ }
+ 5: (#struct){
+ name: (string){ "3" }
+ }
+ }
+ }
+}