cue: closedness only applies to regular fields

Issue #40

Change-Id: I6f2cf753e84bc570dfe1fc91cda1eccb29f0582e
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3042
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/binop.go b/cue/binop.go
index 99ad043..b9b6149 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -653,7 +653,7 @@
 				break
 			}
 		}
-		if !unchecked && !found && !y.allows(a.feature) {
+		if !unchecked && !found && !y.allows(a.feature) && !a.definition {
 			if a.optional {
 				continue
 			}
@@ -691,7 +691,7 @@
 				continue outer
 			}
 		}
-		if !unchecked && !found && !x.allows(a.feature) {
+		if !unchecked && !found && !x.allows(a.feature) && !a.definition {
 			if a.optional {
 				continue
 			}
diff --git a/cue/export.go b/cue/export.go
index 9ea6c8b..8cd1518 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -215,6 +215,11 @@
 }
 
 func (p *exporter) closeOrOpen(s *ast.StructLit, isClosed bool) ast.Expr {
+	// Note, there is no point in printing close if we are dropping optional
+	// fields, as by this the meaning of close will change anyway.
+	if p.mode.omitOptional || p.mode.concrete {
+		return s
+	}
 	if isClosed && !p.inDef {
 		return &ast.CallExpr{
 			Fun:  ast.NewIdent("close"),
diff --git a/cue/export_test.go b/cue/export_test.go
index 760a135..a8c7bc3 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -27,6 +27,7 @@
 	testCases := []struct {
 		raw     bool // skip evaluation the root, fully raw
 		eval    bool // evaluate the full export
+		noOpt   bool
 		in, out string
 	}{{
 		in:  `"hello"`,
@@ -100,30 +101,6 @@
 				e: _|_ /* undefined field "t" */
 			}`),
 	}, {
-		// a closed struct with template restrictions is exported as a
-		// conjunction of two structs.
-		in: `{
-			A :: { b: int }
-			a: A & { <_>: <10 }
-			B :: a
-		}`,
-		out: unindent(`
-		{
-			A :: {
-				b: int
-			}
-			a: close({
-				b: <10
-			}) & {
-				<_>: <10
-			}
-			B :: {
-				b: <10
-			} & {
-				<_>: <10
-			}
-		}`),
-	}, {
 		in: `{
 			a: 5*[int]
 			a: [1, 2, ...]
@@ -383,8 +360,9 @@
 			}
 		}`),
 	}, {
-		raw:  true,
-		eval: true,
+		raw:   true,
+		eval:  true,
+		noOpt: true,
 		in: `{
 				reg: { foo: 1, bar: { baz: 3 } }
 				def :: {
@@ -404,42 +382,38 @@
 				a: 1
 				sub: {
 					foo: 1
-					bar: {
-						baz: 3
-						...
-					}
-					...
+					bar baz: 3
 				}
 			}
-			val: close({
+			val: {
 				a: 1
 				sub: {
 					foo: 1
 					bar baz: 3
 				}
-			})
+			}
 		}`),
 	}, {
 		raw:  true,
 		eval: true,
 		in: `{
-				b: [{
-					<X>: int
-					f: 4 if a > 4
-				}][a]
-				a: int
-				c: *1 | 2
-			}`,
+			b: [{
+				<X>: int
+				f: 4 if a > 4
+			}][a]
+			a: int
+			c: *1 | 2
+		}`,
 		// reference to a must be redirected to outer a through alias
 		out: unindent(`
-			{
-				b: [{
-					<X>: int
-					f:   4 if a > 4
-				}][a]
-				a: int
-				c: 1
-			}`),
+		{
+			b: [{
+				<X>: int
+				f:   4 if a > 4
+			}][a]
+			a: int
+			c: 1
+		}`),
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
@@ -455,7 +429,7 @@
 			n := root.(*structLit).arcs[0].v
 			v := newValueRoot(ctx, n)
 
-			opts := options{raw: !tc.eval}
+			opts := options{raw: !tc.eval, omitOptional: true}
 			node, _ := export(ctx, v.eval(ctx), opts)
 			b, err := format.Node(node, format.Simplify())
 			if err != nil {
@@ -595,6 +569,67 @@
 		} | {
 			c: time.Duration
 		}`),
+	}, {
+		// a closed struct unified with a struct with a template restrictions is
+		// exported as a conjunction of two structs.
+		eval: true,
+		in: `
+		A :: { b: int }
+		a: A & { <_>: <10 }
+		B :: a
+		`,
+		out: unindent(`
+		{
+			A :: {
+				b: int
+			}
+			a: close({
+				b: <10
+			}) & {
+				<_>: <10
+			}
+			B :: {
+				b: <10
+			} & {
+				<_>: <10
+			}
+		}`),
+	}, {
+		eval: true,
+		in: `{
+			reg: { foo: 1, bar: { baz: 3 } }
+			def :: {
+				a: 1
+
+				sub: reg
+			}
+			val: def
+		}`,
+		out: unindent(`
+		{
+			reg: {
+				foo: 1
+				bar baz: 3
+			}
+			def :: {
+				a: 1
+				sub: {
+					foo: 1
+					bar: {
+						baz: 3
+						...
+					}
+					...
+				}
+			}
+			val: close({
+				a: 1
+				sub: {
+					foo: 1
+					bar baz: 3
+				}
+			})
+		}`),
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
@@ -603,7 +638,11 @@
 			if err != nil {
 				t.Fatal(err)
 			}
-			b, err := format.Node(inst.Value().Syntax(Raw()), format.Simplify())
+			opts := []Option{}
+			if !tc.eval {
+				opts = []Option{Raw()}
+			}
+			b, err := format.Node(inst.Value().Syntax(opts...), format.Simplify())
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cue/value.go b/cue/value.go
index ed49e71..8ce8537 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -658,7 +658,7 @@
 }
 
 func (x *structLit) allows(f label) bool {
-	return !x.isClosed
+	return !x.isClosed || f&hidden != 0
 }
 
 func newStruct(src source) *structLit {
@@ -886,7 +886,7 @@
 // A label is a canonicalized feature name.
 type label uint32
 
-const hidden label = 0x01 // only set iff identifier starting with $
+const hidden label = 0x01 // only set iff identifier starting with _
 
 // An arc holds the label-value pair.
 //
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index 48393ff..453c77e 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -1122,7 +1122,7 @@
 
 By default, structs are open to adding fields.
 Instances of an open struct `p` may contain fields not defined in `p`.
-A _closed struct_ `c` is a struct whose instances may not have fields
+A _closed struct_ `c` is a struct whose instances may not have regular fields
 not defined in `c`.
 Closing a struct is equivalent to adding an optional field with value `_|_`
 for all undefined fields.
@@ -1190,7 +1190,7 @@
 or as a _definition_ (using `::`).
 Definitions are not emitted as part of the model and are never required
 to be concrete when emitting data.
-It is illegal to have a normal field and a definition with the same name
+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.
 An ellipsis `...` in such literal structs keeps them open,
@@ -1517,7 +1517,7 @@
 All other identifiers are not exported.
 
 An identifier that starts with the underscore "_" is not
-emitted in any data output.
+emitted in any data output and treated as a definition for that purpose.
 Quoted labels that start with an underscore are emitted, however.
 <!-- END REPLACE -->