cue: relax rules for closed structs
Closedness constraints of literal structs now don't take
effect until after a field expression is evaluated.
This is only part of the implementatation. Several
aspects are implemented in follow-up CLs.
Change-Id: I588223cd555f8cde537ade7caf1901e6e425291a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3640
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/vet_expr.txt b/cmd/cue/cmd/testdata/script/vet_expr.txt
index e96888c..5381795 100644
--- a/cmd/cue/cmd/testdata/script/vet_expr.txt
+++ b/cmd/cue/cmd/testdata/script/vet_expr.txt
@@ -2,14 +2,19 @@
cmp stderr expect-stderr
-- expect-stderr --
+translations.hello.lang: incomplete value (string):
+ ./vet.cue:3:11
+translations.hello.lang: conflicting values false and string (mismatched types bool and string):
+ ./data.yaml:13:11
+ ./vet.cue:3:11
field "skip" not allowed in closed struct:
- ./vet.cue:4:9
+ ./vet.cue:1:9
-- vet.cue --
-
-translations <_> lang: string
-
File :: {
- translations: {...}
+ translations <_>: {
+ lang: string
+ text: string
+ }
}
-- data.yaml --
# translated messages
diff --git a/cue/ast.go b/cue/ast.go
index a4ac89f..f134276 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -277,7 +277,9 @@
if v.ctx().inDefinition > 0 {
// For embeddings this is handled in binOp, in which case the
// isClosed bit is cleared if a template is introduced.
- obj.isClosed = obj.template == nil
+ if obj.template == nil {
+ obj.closeStatus = toClose
+ }
}
if passDoc {
v.doc = v1.doc // signal usage of document back to parent.
diff --git a/cue/binop.go b/cue/binop.go
index 0bec239..152b8ac 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -625,7 +625,7 @@
binSrc(src.Pos(), op, x, other), // baseValue
x.emit, // emit
nil, // template
- x.isClosed || y.isClosed, // isClosed
+ x.closeStatus | y.closeStatus, // closeStatus
nil, // comprehensions
arcs, // arcs
nil, // attributes
@@ -725,7 +725,7 @@
sort.Stable(obj)
if unchecked && obj.template != nil {
- obj.isClosed = false
+ obj.closeStatus = 0
}
return obj
diff --git a/cue/copy.go b/cue/copy.go
index 335a29d..6c80735 100644
--- a/cue/copy.go
+++ b/cue/copy.go
@@ -31,7 +31,7 @@
case *structLit:
arcs := make(arcs, len(x.arcs))
- obj := &structLit{x.baseValue, nil, nil, x.isClosed, nil, arcs, nil}
+ obj := &structLit{x.baseValue, nil, nil, x.closeStatus, nil, arcs, nil}
defer ctx.pushForwards(x, obj).popForwards()
diff --git a/cue/debug.go b/cue/debug.go
index bb130bf..ca3e970 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -273,7 +273,7 @@
if p.showNodeRef {
p.writef("<%s>", p.ctx.ref(x))
}
- if x.isClosed {
+ if x.closeStatus != 0 {
write("C")
}
write("{")
@@ -301,7 +301,7 @@
p.write(", ")
}
}
- if topDefault && !x.isClosed {
+ if topDefault && x.closeStatus == 0 {
if len(x.arcs) > 0 {
p.write(", ")
}
diff --git a/cue/eval.go b/cue/eval.go
index 7a3dea9..59e1ffd 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -67,7 +67,7 @@
}
if n.val() == nil {
field := ctx.labelStr(x.feature)
- if st, ok := sc.(*structLit); ok && !st.isClosed {
+ if st, ok := sc.(*structLit); ok && !st.isClosed() {
return ctx.mkErr(x, codeIncomplete, "undefined field %q", field)
}
// m.foo undefined (type map[string]bool has no field or method foo)
@@ -109,7 +109,7 @@
return ctx.mkErr(x, index, codeIncomplete, "field %q is optional", s)
}
if n.val() == nil {
- if !v.isClosed {
+ if !v.isClosed() {
return ctx.mkErr(x, index, codeIncomplete, "undefined field %q", s)
}
return ctx.mkErr(x, index, "undefined field %q", s)
diff --git a/cue/export.go b/cue/export.go
index e599c31..86376c1 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -422,13 +422,13 @@
return bin
case *structLit:
- st, err := p.structure(x, !x.isClosed)
+ st, err := p.structure(x, !x.isClosed())
if err != nil {
return p.expr(err)
}
- expr := p.closeOrOpen(st, x.isClosed)
+ expr := p.closeOrOpen(st, x.isClosed())
switch {
- case x.isClosed && x.template != nil:
+ case x.isClosed() && x.template != nil:
l, ok := x.template.evalPartial(p.ctx).(*lambdaExpr)
if !ok {
break
@@ -632,7 +632,7 @@
case !doEval(p.mode) && x.template != nil && addTempl:
l, ok := x.template.evalPartial(p.ctx).(*lambdaExpr)
if ok {
- if _, ok := l.value.(*top); ok && !x.isClosed {
+ if _, ok := l.value.(*top); ok && !x.isClosed() {
break
}
obj.Elts = append(obj.Elts, &ast.Field{
@@ -735,7 +735,7 @@
break
}
s.Elts = append(s.Elts, st.Elts...)
- return x.isClosed
+ return x.isClosed()
case *binaryExpr:
if x.op != opUnifyUnchecked {
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index f3bac0a..9572a63 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -1155,6 +1155,7 @@
}
}
+ // Allowed
Foo1 :: { field: int }
Foo1 :: { field2: string }
@@ -1184,14 +1185,56 @@
`,
out: `<0>{` +
`Foo :: <1>C{field: int, recursive: <2>C{field: string}}, ` +
- `Foo1 :: _|_((<3>C{field: int} & <4>C{field2: string}):field "field" not allowed in closed struct), ` +
+ `Foo1 :: <3>C{field: int, field2: string}, ` +
`foo: _|_(2:field "feild" not allowed in closed struct), ` +
- `foo1: <5>C{field: 2, recursive: _|_(2:field "feild" not allowed in closed struct)}, ` +
- `Bar :: <6>{<>: <7>(A: string)->int, field: int}, ` +
- `bar: <8>{<>: <9>(A: string)->int, field: int, feild: 2}, ` +
+ `foo1: <4>C{field: 2, recursive: _|_(2:field "feild" not allowed in closed struct)}, ` +
+ `Bar :: <5>{<>: <6>(A: string)->int, field: int}, ` +
+ `bar: <7>{<>: <8>(A: string)->int, field: int, feild: 2}, ` +
`Mixed: _|_(field "Mixed" declared as definition and regular field), ` +
`mixedRec: _|_(field "Mixed" declared as definition and regular field)}`,
}, {
+ desc: "combined definitions",
+ in: `
+ // Allow combining of structs within a definition
+ D1 :: {
+ env a: "A"
+ env b: "B"
+ def :: {a: "A"}
+ def :: {b: "B"}
+ }
+
+ d1: D1 & { env c: "C" }
+
+ D2 :: {
+ a: int
+ }
+ D2 :: {
+ b: int
+ }
+
+ D3 :: {
+ env a: "A"
+ }
+ D3 :: {
+ env b: "B"
+ }
+
+ D4 :: {
+ env: DC
+ env b: int
+ }
+
+ DC :: { a: int }
+ `,
+ out: `<0>{` +
+ `D1 :: <1>C{env: <2>C{a: "A", b: "B"}, def :: <3>C{a: "A", b: "B"}}, ` +
+ `d1: <4>C{env: <5>C{a: "A", b: "B", c: "C"}, def :: <6>C{a: "A", b: "B"}}, ` +
+ `D2 :: <7>C{a: int, b: int}, ` +
+ `D3 :: <8>C{env: <9>C{a: "A", b: "B"}}, ` +
+ `D4 :: <10>C{env: _|_(int:field "b" not allowed in closed struct)}, ` +
+ `DC :: <11>C{a: int}` +
+ `}`,
+ }, {
desc: "definitions with oneofs",
in: `
Foo :: {
@@ -1228,20 +1271,16 @@
b: 3
}
- // error: literal struct is closed before unify
- e1 :: S & { a c: 4 }
- // no such issue here
- v1: S & { a c: 4 }
-
// adding a field to a nested struct that is closed.
- e2: S & { a d: 4 }
+ e1 :: S & { a d: 4 }
+ // literal struct not closed until after unification.
+ v1 :: S & { a c: 4 }
`,
out: `<0>{` +
`E :: <1>C{a: <2>C{b: int}}, ` +
`S :: <3>C{a: <4>C{b: int, c: int}, b: 3}, ` +
- `e1 :: _|_((<5>.S & <6>C{a: <7>C{c: 4}}):field "b" not allowed in closed struct), ` +
- `v1: <8>C{a: <9>C{b: int, c: 4}, b: 3}, ` +
- `e2: <10>C{a: _|_(4:field "d" not allowed in closed struct), b: 3}}`,
+ `e1 :: <5>C{a: _|_(4:field "d" not allowed in closed struct), b: 3}, ` +
+ `v1 :: <6>C{a: <7>C{b: int, c: 4}, b: 3}}`,
}, {
desc: "closing structs",
in: `
@@ -1310,8 +1349,8 @@
}, {
desc: "closing with failed optional",
in: `
- k1 :: {a: int, b?: int} & {a: int} // closed({a: int})
- k2 :: {a: int} & {a: int, b?: int} // closed({a: int})
+ k1 :: {a: int, b?: int} & A // closed({a: int})
+ k2 :: A & {a: int, b?: int} // closed({a: int})
o1: {a?: 3} & {a?: 4} // {a?: _|_}
@@ -1320,14 +1359,18 @@
d1 :: {a?: 2, b: 4} | {a?: 3, c: 5}
v1: d1 & {a?: 3, b: 4} // close({b: 4})
+
+ A :: {a: int}
`,
out: `<0>{` +
`k1 :: <1>C{a: int}, ` +
- `k2 :: <2>C{a: int}, ` +
- `o1: <3>{a?: _|_((3 & 4):conflicting values 3 and 4)}, ` +
- `o2 :: <4>C{a?: _|_((3 & 4):conflicting values 3 and 4)}, ` +
- `d1 :: (<5>C{a?: 2, b: 4} | <6>C{a?: 3, c: 5}), ` +
- `v1: <7>C{a?: _|_((2 & 3):conflicting values 2 and 3), b: 4}}`,
+ `A :: <2>C{a: int}, ` +
+ `k2 :: <3>C{a: int}, ` +
+ `o1: <4>{a?: _|_((3 & 4):conflicting values 3 and 4)}, ` +
+ `o2 :: <5>C{a?: _|_((3 & 4):conflicting values 3 and 4)}, ` +
+ `d1 :: (<6>C{a?: 2, b: 4} | <7>C{a?: 3, c: 5}), ` +
+ `v1: <8>C{a?: _|_((2 & 3):conflicting values 2 and 3), b: 4}` +
+ `}`,
}, {
desc: "closing with comprehensions",
in: `
@@ -2392,7 +2435,12 @@
// Don't remove. For debugging.
testCases := []testCase{{
- in: ``,
+ in: `
+ Foo :: { a: int }
+
+ Foo
+ b: int
+ `,
}}
rewriteHelper(t, testCases, evalFull)
}
diff --git a/cue/rewrite_test.go b/cue/rewrite_test.go
index 0e1fa08..2673e41 100644
--- a/cue/rewrite_test.go
+++ b/cue/rewrite_test.go
@@ -75,7 +75,7 @@
t = v
}
emit := testResolve(ctx, x.emit, m)
- obj := &structLit{x.baseValue, emit, t, x.isClosed, nil, arcs, nil}
+ obj := &structLit{x.baseValue, emit, t, x.closeStatus, nil, arcs, nil}
return obj
case *list:
elm := rewriteRec(ctx, x.elem, x.elem, m).(*structLit)
diff --git a/cue/subsume.go b/cue/subsume.go
index cacc6fa..1c928e0 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -113,7 +113,7 @@
}
}
// For closed structs, all arcs in b must exist in a.
- if x.isClosed {
+ if x.isClosed() {
for _, b := range o.arcs {
a := x.lookup(ctx, b.feature)
if a.val() == nil {
diff --git a/cue/types.go b/cue/types.go
index 4fe7942..34edee1 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -1148,13 +1148,13 @@
}
arcs = arcs[:k]
obj = &structLit{
- obj.baseValue, // baseValue
- obj.emit, // emit
- obj.template, // template
- obj.isClosed, // isClosed
- nil, // comprehensions
- arcs, // arcs
- nil, // attributes
+ obj.baseValue, // baseValue
+ obj.emit, // emit
+ obj.template, // template
+ obj.closeStatus, // isClosed
+ nil, // comprehensions
+ arcs, // arcs
+ nil, // attributes
}
}
return structValue{ctx, v.path, obj}, nil
diff --git a/cue/value.go b/cue/value.go
index 519408b..f3cf133 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -639,8 +639,8 @@
// template must evaluated to a lambda and is applied to all concrete
// values in the struct, whether it be open or closed.
- template value
- isClosed bool
+ template value
+ closeStatus byte
comprehensions []value
@@ -649,6 +649,15 @@
expanded evaluated
}
+func (x *structLit) isClosed() bool {
+ return x.closeStatus != 0
+}
+
+const (
+ toClose = 1
+ isClosed = 2
+)
+
func (x *structLit) addTemplate(ctx *context, pos token.Pos, t value) {
if x.template == nil {
x.template = t
@@ -658,7 +667,7 @@
}
func (x *structLit) allows(f label) bool {
- return !x.isClosed || f&hidden != 0
+ return x.closeStatus&isClosed == 0 || f&hidden != 0
}
func newStruct(src source) *structLit {
@@ -679,7 +688,7 @@
}
newS := *x
- newS.isClosed = true
+ newS.closeStatus = isClosed
return &newS
}
@@ -767,6 +776,11 @@
// it is safe to cache the result.
ctx.cycleErr = false
+ if s, ok := v.(*structLit); ok {
+ if s.closeStatus != 0 {
+ s.closeStatus = 2
+ }
+ }
x.arcs[i].cache = v
if doc != nil {
x.arcs[i].docs = &docNode{n: doc, left: x.arcs[i].docs}
@@ -788,6 +802,10 @@
// should not be evaluated until the other fields of a struct are
// fully evaluated.
func (x *structLit) expandFields(ctx *context) (st *structLit, err *bottom) {
+ if x.closeStatus == toClose {
+ x.closeStatus = isClosed
+ }
+
switch v := x.expanded.(type) {
case nil:
case *structLit:
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index cc6ad2c..f07149d 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -1330,8 +1330,18 @@
to be concrete when emitting data.
It is illegal to have a regular field and a definition with the same name
within the same struct.
-Literal structs that are part of a definition's value are implicitly closed.
+Literal structs that are part of a definition's value are implicitly closed,
+but may unify unrestricted with other structs within the field's declaration.
This excludes literals structs in embeddings and aliases.
+<!--
+This may be a more intuitive definition:
+ Literal structs that are part of a definition's value are implicitly closed.
+ Implicitly closed literal structs that are unified within
+ a single field declaration are considered to be a single literal struct.
+However, this would make unification non-commutative, unless one imposes an
+ordering where literal structs are unified before unifying them with others.
+Imposing such an ordering is complex and error prone.
+-->
An ellipsis `...` in such literal structs keeps them open,
as it defines `_` for all labels.
<!--
@@ -1359,22 +1369,17 @@
-->
```
-// MyStruct is closed and as there is no expression label or `...`, we know
-// this is the full definition.
MyStruct :: {
- field: string
- enabled?: bool
+ sub field: string
}
-// Without the `...`, this field would not unify with its previous declaration.
MyStruct :: {
- enabled: bool | *false
- ...
+ sub enabled?: bool
}
myValue: MyStruct & {
- feild: 2 // error, feild not defined in MyStruct
- enabled: true // okay
+ sub feild: 2 // error, feild not defined in MyStruct
+ sub enabled: true // okay
}
D :: {
@@ -1441,6 +1446,7 @@
// attr: int @xml(,attr) @xml(a1,attr) @go(Attr)
```
+
#### Aliases
In addition to fields, a struct literal may also define aliases.