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.