cue: add optional fields

Optional fields are crucial in making it manageable
to work with very large struct types, like those of
Kubernetes. Without optional fields, the fields of
a simple K8s configuration  would be lost in the noise
compared to the default value of every expanded field.

It also allows writing:
  foo?: MyStruct
instead of
  foo: *null | MyStruct
which gets old quickly when one has hundreds of
such fields.

Updates #24

Change-Id: I856d9a3e20584750b911784924fa18d4557b9920
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1700
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/ast.go b/cue/ast.go
index b6924b4..5e02b9e 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -112,7 +112,7 @@
 	ctx := v.ctx()
 	label := v.label(n.Name, true)
 	if r := v.resolveRoot; r != nil {
-		if value, _ := r.lookup(v.ctx(), label); value != nil {
+		if a := r.lookup(v.ctx(), label); a.val() != nil {
 			return &selectorExpr{newExpr(n),
 				&nodeRef{baseValue: newExpr(n), node: r}, label}
 		}
@@ -252,6 +252,7 @@
 		v.object.comprehensions = append(v.object.comprehensions, fc)
 
 	case *ast.Field:
+		opt := n.Optional != token.NoPos
 		switch x := n.Label.(type) {
 		case *ast.Interpolation:
 			yielder := &yield{baseValue: newNode(x)}
@@ -261,6 +262,7 @@
 			}
 			yielder.key = v.walk(x)
 			yielder.value = v.walk(n.Value)
+			yielder.opt = opt
 			v.object.comprehensions = append(v.object.comprehensions, fc)
 
 		case *ast.TemplateLabel:
@@ -289,7 +291,7 @@
 				return v.error(n.Label, "invalid field name: %v", n.Label)
 			}
 			if f != 0 {
-				v.object.insertValue(v.ctx(), f, v.walk(n.Value), attrs)
+				v.object.insertValue(v.ctx(), f, opt, v.walk(n.Value), attrs)
 			}
 
 		default:
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index b7d05c6..cc2e7eb 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -277,7 +277,8 @@
 // A Field represents a field declaration in a struct.
 type Field struct {
 	comments
-	Label Label // must have at least one element.
+	Label    Label // must have at least one element.
+	Optional token.Pos
 
 	// No colon: Value must be an StructLit with one field.
 	Colon token.Pos
diff --git a/cue/binop.go b/cue/binop.go
index 52f906c..2cbd9aa 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -530,7 +530,8 @@
 
 	for _, a := range x.arcs {
 		cp := ctx.copy(a.v)
-		obj.arcs = append(obj.arcs, arc{a.feature, cp, nil, a.attrs})
+		obj.arcs = append(obj.arcs,
+			arc{a.feature, a.optional, cp, nil, a.attrs})
 	}
 outer:
 	for _, a := range y.arcs {
@@ -539,6 +540,8 @@
 			if a.feature == b.feature {
 				v = mkBin(ctx, src.Pos(), opUnify, b.v, v)
 				obj.arcs[i].v = v
+				obj.arcs[i].cache = nil
+				obj.arcs[i].optional = a.optional && b.optional
 				attrs, err := unifyAttrs(ctx, src, a.attrs, b.attrs)
 				if err != nil {
 					return err
@@ -547,7 +550,8 @@
 				continue outer
 			}
 		}
-		obj.arcs = append(obj.arcs, arc{feature: a.feature, v: v, attrs: a.attrs})
+		a.setValue(v)
+		obj.arcs = append(obj.arcs, a)
 	}
 	sort.Stable(obj)
 
@@ -1054,7 +1058,7 @@
 			if isBottom(v) {
 				return v
 			}
-			arcs[i] = arc{x.arcs[i].feature, v, nil, nil}
+			arcs[i] = arc{feature: x.arcs[i].feature, v: v}
 		}
 
 		return lambda
diff --git a/cue/copy.go b/cue/copy.go
index edefaa0..75011c3 100644
--- a/cue/copy.go
+++ b/cue/copy.go
@@ -52,8 +52,8 @@
 		obj.template = t
 
 		for i, a := range x.arcs {
-			v := ctx.copy(a.v)
-			arcs[i] = arc{a.feature, v, nil, a.attrs}
+			a.setValue(ctx.copy(a.v))
+			arcs[i] = a
 		}
 
 		comp := make([]*fieldComprehension, len(x.comprehensions))
diff --git a/cue/debug.go b/cue/debug.go
index a9dec8d..136379d 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -269,6 +269,9 @@
 			str = str[1 : len(str)-1]
 		}
 		p.writef(str)
+		if x.optional {
+			p.write("?")
+		}
 		p.write(": ")
 		p.debugStr(n)
 		if x.attrs != nil {
@@ -289,6 +292,9 @@
 		writef(" yield ")
 		writef("(")
 		p.debugStr(x.key)
+		if x.opt {
+			writef("?")
+		}
 		writef("): ")
 		p.debugStr(x.value)
 
diff --git a/cue/eval.go b/cue/eval.go
index d7ee66a..b908013 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -56,13 +56,14 @@
 	v := e.eval(x.x, structKind|lambdaKind, msgType, x)
 
 	if e.is(v, structKind|lambdaKind, "") {
-		n, _ := v.(scope).lookup(ctx, x.feature)
-		if n == nil {
+		n := v.(scope).lookup(ctx, x.feature)
+		if n.val() == nil {
 			field := ctx.labelStr(x.feature)
 			//	m.foo undefined (type map[string]bool has no field or method foo)
 			return ctx.mkErr(x, "undefined field %q", field)
 		}
-		return n.evalPartial(ctx)
+		// TODO: do we need to evaluate here?
+		return n.cache.evalPartial(ctx)
 	}
 	return e.err(&selectorExpr{x.baseValue, v, x.feature})
 }
@@ -87,11 +88,11 @@
 		if e.is(index, stringKind, msgIndexType, k) {
 			s := index.strValue()
 			// TODO: must lookup
-			n, _ := v.lookup(ctx, ctx.strLabel(s))
-			if n == nil {
+			n := v.lookup(ctx, ctx.strLabel(s))
+			if n.val() == nil {
 				return ctx.mkErr(x, index, "undefined field %q", s)
 			}
-			return n
+			return n.cache
 		}
 	case atter:
 		if e.is(index, intKind, msgIndexType, k) {
@@ -241,7 +242,7 @@
 
 func (x *listComprehension) evalPartial(ctx *context) evaluated {
 	list := &list{baseValue: x.baseValue}
-	result := x.clauses.yield(ctx, func(k, v evaluated) *bottom {
+	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")
 		}
diff --git a/cue/export.go b/cue/export.go
index 4de8767..e5111d2 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -284,9 +284,14 @@
 						// TODO: add an invalid field instead?
 						continue
 					}
+					opt := token.NoPos
+					if yield.opt {
+						opt = 1 // anything but token.NoPos
+					}
 					f := &ast.Field{
-						Label: label,
-						Value: p.expr(yield.value),
+						Label:    label,
+						Optional: opt,
+						Value:    p.expr(yield.value),
 					}
 					var decl ast.Decl = f
 					if len(clauses) > 0 {
diff --git a/cue/format/format.go b/cue/format/format.go
index c2691c5..30df812 100644
--- a/cue/format/format.go
+++ b/cue/format/format.go
@@ -174,7 +174,12 @@
 	current  frame
 	nestExpr int
 
-	labelBuf []ast.Label
+	labelBuf []labelEntry
+}
+
+type labelEntry struct {
+	label    ast.Label
+	optional bool
 }
 
 func newFormatter(p *printer) *formatter {
diff --git a/cue/format/node.go b/cue/format/node.go
index 3199995..8c19372 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -115,7 +115,7 @@
 		// shortcut single-element structs.
 		lastSize := len(f.labelBuf)
 		f.labelBuf = f.labelBuf[:0]
-		first := n.Label
+		first, opt := n.Label, n.Optional != token.NoPos
 		for {
 			obj, ok := n.Value.(*ast.StructLit)
 			if !ok || len(obj.Elts) != 1 || (obj.Lbrace.IsValid() && !f.printer.cfg.simplify) {
@@ -138,7 +138,8 @@
 			if !ok {
 				break
 			}
-			f.labelBuf = append(f.labelBuf, mem.Label)
+			entry := labelEntry{mem.Label, mem.Optional != token.NoPos}
+			f.labelBuf = append(f.labelBuf, entry)
 			n = mem
 		}
 
@@ -147,10 +148,10 @@
 		}
 
 		f.before(nil)
-		f.label(first)
+		f.label(first, opt)
 		for _, x := range f.labelBuf {
 			f.print(blank, nooverride)
-			f.label(x)
+			f.label(x.label, x.optional)
 		}
 		f.after(nil)
 
@@ -243,7 +244,7 @@
 
 func (f *formatter) importSpec(x *ast.ImportSpec) {
 	if x.Name != nil {
-		f.label(x.Name)
+		f.label(x.Name, false)
 		f.print(blank)
 	} else {
 		f.current.pos++
@@ -253,7 +254,7 @@
 	f.print(newline)
 }
 
-func (f *formatter) label(l ast.Label) {
+func (f *formatter) label(l ast.Label, optional bool) {
 	switch n := l.(type) {
 	case *ast.Ident:
 		f.print(n.NamePos, n)
@@ -275,7 +276,7 @@
 
 	case *ast.TemplateLabel:
 		f.print(n.Langle, token.LSS, indent)
-		f.label(n.Ident)
+		f.label(n.Ident, false)
 		f.print(unindent, n.Rangle, token.GTR)
 
 	case *ast.Interpolation:
@@ -284,6 +285,9 @@
 	default:
 		panic(fmt.Sprintf("unknown label type %T", n))
 	}
+	if optional {
+		f.print(token.OPTION)
+	}
 }
 
 func (f *formatter) expr(x ast.Expr) {
@@ -436,13 +440,13 @@
 	case *ast.ForClause:
 		f.print(blank, n.For, "for", blank)
 		if n.Key != nil {
-			f.label(n.Key)
+			f.label(n.Key, false)
 			f.print(n.Colon, token.COMMA, blank)
 		} else {
 			f.current.pos++
 			f.visitComments(f.current.pos)
 		}
-		f.label(n.Value)
+		f.label(n.Value, false)
 		f.print(blank, n.In, "in", blank)
 		f.expr(n.Source)
 
diff --git a/cue/format/testdata/expressions.golden b/cue/format/testdata/expressions.golden
index 830b215..15480aa 100644
--- a/cue/format/testdata/expressions.golden
+++ b/cue/format/testdata/expressions.golden
@@ -6,10 +6,11 @@
 
 	b: 3
 
-	c b a:    4
-	c bb aaa: 5
+	c b a:       4
+	c? bb? aaa?: 5
 	c b <Name> a: int
 	alias = 3.14
+	"g\("en")"?: 4
 
 	alias2 = foo
 	aaalias = foo
diff --git a/cue/format/testdata/expressions.input b/cue/format/testdata/expressions.input
index f15e37e..cebb633 100644
--- a/cue/format/testdata/expressions.input
+++ b/cue/format/testdata/expressions.input
@@ -7,9 +7,10 @@
     b: 3
 
     c b a:  4
-    c bb aaa: 5
+    c? bb? aaa?: 5
     c b <Name> a: int
     alias = 3.14
+    "g\("en")"?: 4
 
     alias2 = foo
     aaalias = foo
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 74dab9c..e8ce7a5 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -733,6 +733,11 @@
 			}
 		}
 
+		if tok != token.LSS && p.tok == token.OPTION {
+			m.Optional = p.pos
+			p.next()
+		}
+
 		if p.tok == token.COLON {
 			break
 		}
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index 52364d5..32c0bf8 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -77,10 +77,12 @@
 	}, {
 		"not emitted",
 		`a: true
-		 b: "2"
-		 c: 3
+		 b?: "2"
+		 c?: 3
+
+		 "g\("en")"?: 4
 		`,
-		`a: true, b: "2", c: 3`,
+		`a: true, b?: "2", c?: 3, "g\("en")"?: 4`,
 	}, {
 		"emitted referencing non-emitted",
 		`a: 1
diff --git a/cue/parser/print.go b/cue/parser/print.go
index 4bdcc13..4c9b3a0 100644
--- a/cue/parser/print.go
+++ b/cue/parser/print.go
@@ -137,6 +137,9 @@
 
 	case *ast.Field:
 		out := debugStr(v.Label)
+		if v.Optional != token.NoPos {
+			out += "?"
+		}
 		if v.Value != nil {
 			out += ": "
 			out += debugStr(v.Value)
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index f438981..1fe8383 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -619,6 +619,24 @@
 			`c: <3>{foo: 1 @bar() @baz(1) @foo()}, ` +
 			`e: _|_((<4>.a & <5>{foo: 1 @foo(other)}):conflicting attributes for key "foo")}`,
 	}, {
+		desc: "optional fields",
+		in: `
+			a: { foo?: string }
+			b: { foo: "foo" }
+			c: a & b
+			d: a & { "foo"?: "bar" }
+
+			g1: 1
+			"g\(1)"?: 1
+			"g\(2)"?: 2
+		`,
+		out: `<0>{a: <1>{foo?: string}, ` +
+			`b: <2>{foo: "foo"}, ` +
+			`c: <3>{foo: "foo"}, ` +
+			`d: <4>{foo?: "bar"}, ` +
+			`g1: 1, ` +
+			`g2?: 2}`,
+	}, {
 		desc: "bounds",
 		in: `
 			i1: >1 & 5
diff --git a/cue/rewrite.go b/cue/rewrite.go
index c7a5e93..fe7faeb 100644
--- a/cue/rewrite.go
+++ b/cue/rewrite.go
@@ -43,9 +43,9 @@
 	obj := &structLit{baseValue: x.baseValue, emit: emit, arcs: arcs}
 	changed := emit == x.emit
 	for i, a := range x.arcs {
-		v := rewrite(ctx, a.v, fn)
-		arcs[i] = arc{a.feature, v, nil, a.attrs}
-		changed = changed || arcs[i].v != v
+		a.setValue(rewrite(ctx, a.v, fn))
+		changed = changed || arcs[i].v != a.v
+		arcs[i] = a
 	}
 	if !changed {
 		return x
@@ -228,7 +228,7 @@
 	if key == x.key && value == x.value {
 		return x
 	}
-	return &yield{x.baseValue, key, value}
+	return &yield{x.baseValue, x.opt, key, value}
 }
 
 func (x *guard) rewrite(ctx *context, fn rewriteFunc) value {
diff --git a/cue/rewrite_test.go b/cue/rewrite_test.go
index fbcf2c3..5180509 100644
--- a/cue/rewrite_test.go
+++ b/cue/rewrite_test.go
@@ -78,7 +78,8 @@
 		arcs := make(arcs, len(x.arcs))
 		for i, a := range x.arcs {
 			v := x.at(ctx, i)
-			arcs[i] = arc{a.feature, rewriteRec(ctx, a.v, v, m), nil, a.attrs}
+			a.setValue(rewriteRec(ctx, a.v, v, m))
+			arcs[i] = a
 		}
 		t := x.template
 		if t != nil {
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index c5e182f..c18e50a 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -837,6 +837,9 @@
 		case ';':
 			tok = token.SEMICOLON
 			insertEOL = true
+		case '?':
+			tok = token.OPTION
+			insertEOL = true
 		case '.':
 			if '0' <= s.ch && s.ch <= '9' {
 				insertEOL = true
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index 87746a6..d1a0402 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -149,6 +149,7 @@
 	{token.LBRACE, "{", operator},
 	{token.COMMA, ",", operator},
 	{token.PERIOD, ".", operator},
+	{token.OPTION, "?", operator},
 
 	{token.RPAREN, ")", operator},
 	{token.RBRACK, "]", operator},
@@ -331,28 +332,28 @@
 
 var lines = []string{
 	// ~ indicates a comma present in the source
-	// ? indicates an automatically inserted comma
+	// ^ indicates an automatically inserted comma
 	"",
 	"\ufeff~,", // first BOM is ignored
 	"~,",
-	"foo?\n",
-	"_foo?\n",
-	"123?\n",
-	"1.2?\n",
-	"'x'?\n",
-	"_|_?\n",
-	"_|_?\n",
-	`"x"` + "?\n",
-	"#'x'#?\n",
+	"foo^\n",
+	"_foo^\n",
+	"123^\n",
+	"1.2^\n",
+	"'x'^\n",
+	"_|_^\n",
+	"_|_^\n",
+	`"x"` + "^\n",
+	"#'x'#^\n",
 	`"""
 		foo
-		"""` + "?\n",
+		"""` + "^\n",
 	// `"""
 	// 	foo \(bar)
-	// 	"""` + "?\n",
+	// 	"""` + "^\n",
 	`'''
 		foo
-		'''` + "?\n",
+		'''` + "^\n",
 
 	"+\n",
 	"-\n",
@@ -361,7 +362,7 @@
 	"%\n",
 
 	"&\n",
-	// "&?\n",
+	// "&^\n",
 	"|\n",
 
 	"&&\n",
@@ -389,41 +390,41 @@
 	"~,\n",
 	".\n",
 
-	")?\n",
-	"]?\n",
-	"]]?\n",
-	"}?\n",
-	"}}?\n",
+	")^\n",
+	"]^\n",
+	"]]^\n",
+	"}^\n",
+	"}}^\n",
 	":\n",
-	";?\n",
+	";^\n",
 
-	"true?\n",
-	"false?\n",
-	"null?\n",
+	"true^\n",
+	"false^\n",
+	"null^\n",
 
-	"foo?//comment\n",
-	"foo?//comment",
-	"foo?/*comment*/\n",
-	"foo?/*\n*/",
-	"foo?/*comment*/    \n",
-	"foo?/*\n*/    ",
+	"foo^//comment\n",
+	"foo^//comment",
+	"foo^/*comment*/\n",
+	"foo^/*\n*/",
+	"foo^/*comment*/    \n",
+	"foo^/*\n*/    ",
 
-	"foo    ?// comment\n",
-	"foo    ?// comment",
-	"foo    ?/*comment*/\n",
-	"foo    ?/*\n*/",
-	"foo    ?/*  */ /* \n */ bar?/**/\n",
-	"foo    ?/*0*/ /*1*/ /*2*/\n",
+	"foo    ^// comment\n",
+	"foo    ^// comment",
+	"foo    ^/*comment*/\n",
+	"foo    ^/*\n*/",
+	"foo    ^/*  */ /* \n */ bar^/**/\n",
+	"foo    ^/*0*/ /*1*/ /*2*/\n",
 
-	"foo    ?/*comment*/    \n",
-	"foo    ?/*0*/ /*1*/ /*2*/    \n",
-	"foo	?/**/ /*-------------*/       /*----\n*/bar       ?/*  \n*/baa?\n",
-	"foo    ?/* an EOF terminates a line */",
-	"foo    ?/* an EOF terminates a line */ /*",
-	"foo    ?/* an EOF terminates a line */ //",
+	"foo    ^/*comment*/    \n",
+	"foo    ^/*0*/ /*1*/ /*2*/    \n",
+	"foo	^/**/ /*-------------*/       /*----\n*/bar       ^/*  \n*/baa^\n",
+	"foo    ^/* an EOF terminates a line */",
+	"foo    ^/* an EOF terminates a line */ /*",
+	"foo    ^/* an EOF terminates a line */ //",
 
-	// "package main?\n\nfunc main() {\n\tif {\n\t\treturn /* */ }?\n}?\n",
-	// "package main?",
+	"package main^\n\nfoo: bar^",
+	"package main^",
 }
 
 func TestCommas(t *testing.T) {
@@ -723,7 +724,7 @@
 	err string
 }{
 	{"\a", token.ILLEGAL, 0, "", "illegal character U+0007"},
-	{`?`, token.ILLEGAL, 0, "", "illegal character U+003F '?'"},
+	{`^`, token.ILLEGAL, 0, "", "illegal character U+005E '^'"},
 	{`…`, token.ILLEGAL, 0, "", "illegal character U+2026 '…'"},
 	{`_|`, token.ILLEGAL, 0, "", "illegal token '_|'; expected '_'"},
 
diff --git a/cue/strip.go b/cue/strip.go
index bc90263..9e8ac78 100644
--- a/cue/strip.go
+++ b/cue/strip.go
@@ -34,8 +34,8 @@
 		if x.template != nil {
 			arcs := make(arcs, len(x.arcs))
 			for i, a := range x.arcs {
-				v := rewrite(ctx, x.at(ctx, i), stripRewriter)
-				arcs[i] = arc{a.feature, v, nil, a.attrs}
+				a.setValue(rewrite(ctx, x.at(ctx, i), stripRewriter))
+				arcs[i] = a
 			}
 			// TODO: verify that len(x.comprehensions) == 0
 			return &structLit{x.baseValue, x.emit, nil, nil, arcs, nil}, false
diff --git a/cue/subsume.go b/cue/subsume.go
index 2c94f68..29df434 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -97,8 +97,15 @@
 
 		// all arcs in n must exist in v and its values must subsume.
 		for _, a := range x.arcs {
-			b, _ := o.lookup(ctx, a.feature)
-			if b == nil || !subsumes(ctx, a.v, b, mode) {
+			b := o.lookup(ctx, a.feature)
+			if !a.optional && b.optional {
+				return false
+			} else if b.val() == nil {
+				// If field a is optional and has value top, neither the
+				// omission of the field nor the field defined with any value
+				// may cause unification to fail.
+				return a.optional && isTop(a.v)
+			} else if !subsumes(ctx, a.v, b.val(), mode) {
 				return false
 			}
 		}
diff --git a/cue/subsume_test.go b/cue/subsume_test.go
index 0e61344..70aeaee 100644
--- a/cue/subsume_test.go
+++ b/cue/subsume_test.go
@@ -333,6 +333,43 @@
 		// Disjunctions
 		330: {subsumes: true, in: `a: >5, b: >10 | 8`},
 		331: {subsumes: false, in: `a: >8, b: >10 | 8`},
+
+		// Optional fields
+		// Optional fields defined constraints on fields that are not yet
+		// defined. So even if such a field is not part of the output, it
+		// influences the lattice structure.
+		// For a given A and B, where A and B unify and where A has an optional
+		// field that is not defined in B, the addition of an incompatible
+		// value of that field in B can cause A and B to no longer unify.
+		//
+		400: {subsumes: false, in: `a: {foo: 1}, b: {}`},
+		401: {subsumes: false, in: `a: {foo?: 1}, b: {}`},
+		402: {subsumes: true, in: `a: {}, b: {foo: 1}`},
+		403: {subsumes: true, in: `a: {}, b: {foo?: 1}`},
+
+		404: {subsumes: true, in: `a: {foo: 1}, b: {foo: 1}`},
+		405: {subsumes: true, in: `a: {foo?: 1}, b: {foo: 1}`},
+		406: {subsumes: true, in: `a: {foo?: 1}, b: {foo?: 1}`},
+		407: {subsumes: false, in: `a: {foo: 1}, b: {foo?: 1}`},
+
+		408: {subsumes: false, in: `a: {foo: 1}, b: {foo: 2}`},
+		409: {subsumes: false, in: `a: {foo?: 1}, b: {foo: 2}`},
+		410: {subsumes: false, in: `a: {foo?: 1}, b: {foo?: 2}`},
+		411: {subsumes: false, in: `a: {foo: 1}, b: {foo?: 2}`},
+
+		412: {subsumes: true, in: `a: {foo: number}, b: {foo: 2}`},
+		413: {subsumes: true, in: `a: {foo?: number}, b: {foo: 2}`},
+		414: {subsumes: true, in: `a: {foo?: number}, b: {foo?: 2}`},
+		415: {subsumes: false, in: `a: {foo: number}, b: {foo?: 2}`},
+
+		416: {subsumes: false, in: `a: {foo: 1}, b: {foo: number}`},
+		417: {subsumes: false, in: `a: {foo?: 1}, b: {foo: number}`},
+		418: {subsumes: false, in: `a: {foo?: 1}, b: {foo?: number}`},
+		419: {subsumes: false, in: `a: {foo: 1}, b: {foo?: number}`},
+
+		// The one exception of the rule: there is no value of foo that can be
+		// added to b which would cause the unification of a and b to fail.
+		420: {subsumes: true, in: `a: {foo?: _}, b: {}`},
 	}
 
 	re := regexp.MustCompile(`a: (.*).*b: ([^\n]*)`)
diff --git a/cue/token/token.go b/cue/token/token.go
index 60e10bc..e94859f 100644
--- a/cue/token/token.go
+++ b/cue/token/token.go
@@ -86,8 +86,9 @@
 	RPAREN    // )
 	RBRACK    // ]
 	RBRACE    // }
-	SEMICOLON // :
+	SEMICOLON // ;
 	COLON     // :
+	OPTION    // ?
 	operatorEnd
 
 	keywordBeg
@@ -162,6 +163,7 @@
 	RBRACE:    "}",
 	SEMICOLON: ";",
 	COLON:     ":",
+	OPTION:    "?",
 
 	BOTTOM: "_|_",
 
diff --git a/cue/types.go b/cue/types.go
index 949df68..c11fb73 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -785,6 +785,10 @@
 	return nil, v.checkKind(ctx, stringKind|bytesKind)
 }
 
+// TODO: distinguish between optional, hidden, etc. Probably the best approach
+// is to mark options in context and have a single function for creating
+// a structVal.
+
 // structVal returns an structVal or an error if v is not a struct.
 func (v Value) structVal(ctx *context) (structValue, error) {
 	if err := v.checkKind(ctx, structKind); err != nil {
@@ -796,16 +800,18 @@
 	obj = obj.expandFields(ctx) // expand comprehensions
 
 	// check if any labels are hidden
+	hasOptional := false
 	f := label(0)
 	for _, a := range obj.arcs {
 		f |= a.feature
+		hasOptional = hasOptional || a.optional
 	}
 
-	if f&hidden != 0 {
+	if f&hidden != 0 || hasOptional {
 		arcs := make([]arc, len(obj.arcs))
 		k := 0
 		for _, a := range obj.arcs {
-			if a.feature&hidden == 0 {
+			if a.feature&hidden == 0 && !a.optional {
 				arcs[k] = a
 				k++
 			}
@@ -819,7 +825,6 @@
 			arcs,
 			nil,
 		}
-
 	}
 	return structValue{ctx, v.path, obj}, nil
 }
diff --git a/cue/types_test.go b/cue/types_test.go
index bef35e4..bb33193 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1294,6 +1294,14 @@
 	}, {
 		value: `{a: 2, b: 3, c: ["A", "B"]}`,
 		json:  `{"a":2,"b":3,"c":["A","B"]}`,
+	}, {
+		value: `{foo?: 1, bar?: 2, baz: 3}`,
+		json:  `{"baz":3}`,
+	}, {
+		// Has an unresolved cycle, but should not matter as all fields involved
+		// are optional
+		value: `{foo?: bar, bar?: foo, baz: 3}`,
+		json:  `{"baz":3}`,
 	}}
 	for i, tc := range testCases {
 		t.Run(fmt.Sprintf("%d/%v", i, tc.value), func(t *testing.T) {
diff --git a/cue/value.go b/cue/value.go
index d39a875..c1f442e 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -50,7 +50,7 @@
 
 type scope interface {
 	value
-	lookup(*context, label) (e evaluated, raw value)
+	lookup(*context, label) arc
 }
 
 type atter interface {
@@ -162,8 +162,8 @@
 	return b
 }
 
-func (x baseValue) strValue() string { panic("unimplemented") }
-func (x baseValue) returnKind() kind { panic("unimplemented") }
+func (b baseValue) strValue() string { panic("unimplemented") }
+func (b baseValue) returnKind() kind { panic("unimplemented") }
 
 // top is the top of the value lattice. It subsumes all possible values.
 type top struct{ baseValue }
@@ -592,17 +592,18 @@
 func (x *structLit) Swap(i, j int)      { x.arcs[i], x.arcs[j] = x.arcs[j], x.arcs[i] }
 
 // lookup returns the node for the given label f, if present, or nil otherwise.
-func (x *structLit) lookup(ctx *context, f label) (v evaluated, raw value) {
+func (x *structLit) lookup(ctx *context, f label) arc {
 	x = x.expandFields(ctx)
 	// Lookup is done by selector or index references. Either this is done on
 	// literal nodes or nodes obtained from references. In the later case,
 	// noderef will have ensured that the ancestors were evaluated.
 	for i, a := range x.arcs {
 		if a.feature == f {
-			return x.at(ctx, i), a.v
+			a.cache = x.at(ctx, i)
+			return a
 		}
 	}
-	return nil, nil
+	return arc{}
 }
 
 func (x *structLit) iterAt(ctx *context, i int) arc {
@@ -672,9 +673,11 @@
 	emit := x.emit
 	template := x.template
 	newArcs := []arc{}
+	optional := false
 
 	for _, c := range comprehensions {
-		result := c.clauses.yield(ctx, func(k, v evaluated) *bottom {
+		result := c.clauses.yield(ctx, func(k, v evaluated, opt bool) *bottom {
+			optional = opt
 			if !k.kind().isAnyOf(stringKind) {
 				return ctx.mkErr(k, "key must be of type string")
 			}
@@ -727,11 +730,12 @@
 						ctx.labelStr(f))
 				} else {
 					x.arcs[i].v = mkBin(ctx, x.Pos(), opUnify, a.v, na.v)
+					x.arcs[i].optional = x.arcs[i].optional && optional
 				}
 				continue outer
 			}
 		}
-		x.arcs = append(x.arcs, arc{feature: f, v: na.v})
+		x.arcs = append(x.arcs, arc{feature: f, optional: optional, v: na.v})
 	}
 	sort.Stable(x)
 	return x
@@ -762,13 +766,23 @@
 // however, may have both. In this case, the value must ultimately evaluate
 // to a node, which will then be merged with the existing one.
 type arc struct {
-	feature label
+	feature  label
+	optional bool
 
 	v     value
 	cache evaluated // also used as newValue during unification.
 	attrs *attributes
 }
 
+func (a *arc) val() evaluated {
+	return a.cache
+}
+
+func (a *arc) setValue(v value) {
+	a.v = v
+	a.cache = nil
+}
+
 type arcInfo struct {
 	hidden bool
 	tags   []string // name:string
@@ -777,15 +791,18 @@
 var hiddenArc = &arcInfo{hidden: true}
 
 // insertValue is used during initialization but never during evaluation.
-func (x *structLit) insertValue(ctx *context, f label, value value, a *attributes) {
+func (x *structLit) insertValue(ctx *context, f label, optional bool, value value, a *attributes) {
 	for i, p := range x.arcs {
 		if f != p.feature {
 			continue
 		}
 		x.arcs[i].v = mkBin(ctx, token.NoPos, opUnify, p.v, value)
+		// TODO: should we warn if there is a mixed mode of optional and non
+		// optional fields at this point?
+		x.arcs[i].optional = x.arcs[i].optional && optional
 		return
 	}
-	x.arcs = append(x.arcs, arc{feature: f, v: value, attrs: a})
+	x.arcs = append(x.arcs, arc{f, optional, value, nil, a})
 	sort.Stable(x)
 }
 
@@ -855,7 +872,7 @@
 	if v == nil {
 		panic("nil node")
 	}
-	x.arcs = append(x.arcs, arc{f, v, nil, nil})
+	x.arcs = append(x.arcs, arc{feature: f, v: v})
 }
 
 func (x *params) iterAt(ctx *context, i int) (evaluated, value) {
@@ -877,16 +894,17 @@
 }
 
 // lookup returns the node for the given label f, if present, or nil otherwise.
-func (x *params) lookup(ctx *context, f label) (v evaluated, raw value) {
+func (x *params) lookup(ctx *context, f label) arc {
 	// Lookup is done by selector or index references. Either this is done on
 	// literal nodes or nodes obtained from references. In the later case,
 	// noderef will have ensured that the ancestors were evaluated.
 	for i, a := range x.arcs {
 		if a.feature == f {
-			return x.at(ctx, i), a.v
+			a.cache = x.at(ctx, i)
+			return a
 		}
 	}
-	return nil, nil
+	return arc{}
 }
 
 type lambdaExpr struct {
@@ -917,7 +935,7 @@
 		if isBottom(v) {
 			return v
 		}
-		arcs[i] = arc{a.feature, v, v, nil}
+		arcs[i] = arc{feature: a.feature, v: v, cache: v}
 	}
 	lambda := &lambdaExpr{x.baseValue, &params{arcs}, nil}
 	defer ctx.pushForwards(x, lambda).popForwards()
@@ -1102,7 +1120,7 @@
 	return topKind | nonGround
 }
 
-type yieldFunc func(k, v evaluated) *bottom
+type yieldFunc func(k, v evaluated, optional bool) *bottom
 
 type yielder interface {
 	value
@@ -1111,6 +1129,7 @@
 
 type yield struct {
 	baseValue
+	opt   bool
 	key   value
 	value value
 }
@@ -1131,7 +1150,7 @@
 	if isBottom(v) {
 		return v
 	}
-	if err := fn(k, v); err != nil {
+	if err := fn(k, v, x.opt); err != nil {
 		return err
 	}
 	return nil
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index 7ec05ba..4f5783b 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -992,8 +992,10 @@
 FieldDecl     = Label { Label } ":" Expression { attribute } .
 
 AliasDecl     = Label "=" Expression .
-Label         = identifier | simple_string_lit | TemplateLabel .
 TemplateLabel = "<" identifier ">" .
+ConcreteLabel = identifier | simple_string_lit
+OptionalLabel = ConcreteLabel "?"
+Label         = ConcreteLabel | OptionalLabel | TemplateLabel .
 
 attribute     = "@" identifier "(" attr_elem { "," attr_elem } ")" .
 attr_elem     =  attr_string | identifier "=" attr_string .
@@ -1102,6 +1104,74 @@
 }
 ```
 
+#### Optional fields
+
+An identifier or string label may be followed by a question mark `?`
+to indicate a field is optional.
+Constraints defined by an optional field should only be applied when
+a field is present.
+Fields with such markers may be omitted from output and should not cause
+an error when emitting a concrete configuration, even if its value is
+not concrete or bottom.
+The question mark is not part of the field name.
+The result of unifying two fields only has an optional marker
+if both fields have such a marker.
+
+<!--
+The optional marker solves the issue of having to print large amounts of
+boilerplate when dealing with large types with many optional or default
+values (such as Kubernetes).
+Writing such optional values in terms of *null | value is tedious,
+unpleasant to read, and as it is not well defined what can be dropped or not,
+all null values have to be emitted from the output, even if the user
+doesn't override them.
+Part of the issue is how null is defined. We could adopt a Typescript-like
+approach of introducing "void" or "undefined" to mean "not defined and not
+part of the output". But having all of null, undefined, and void can be
+confusing. If these ever are introduced anyway, the ? operator could be
+expressed along the lines of
+   foo?: bar
+being a shorthand for
+   foo: void | bar
+where void is the default if no other default is given.
+
+The current mechanical definition of "?" is straightforward, though, and
+probably avoids the need for void, while solving a big issue.
+
+Caveats:
+[1] this definition requires explicitly defined fields to be emitted, even
+if they could be elided (for instance if the explicit value is the default
+value defined an optional field). This is probably a good thing.
+
+[2] a default value may still need to be included in an output if it is not
+the zero value for that field and it is not known if any outside system is
+aware of defaults. For instance, which defaults are specified by the user
+and which by the schema understood by the receiving system.
+The use of "?" together with defaults should therefore be used carefully
+in non-schema definitions.
+Problematic cases should be easy to detect by a vet-like check, though.
+
+[3] It should be considered how this affects the trim command.
+Should values implied by optional fields be allowed to be removed?
+Probably not. This restriction is unlikely to limit the usefulness of trim,
+though.
+
+[4] There should be an option to emit all concrete optional values.
+```
+-->
+
+```
+Input                            Result
+a: { foo?: string }              {}
+b: { foo: "bar" }                { foo: "bar" }
+c: { foo?: *"bar" | string }     {}
+
+d: a & b                         { foo: "bar" }
+e: b & c                         { foo: "bar" }
+f: a & c                         {}
+g: a & { foo?: number }          _|_
+```
+
 
 ### Lists
 
diff --git a/go.mod b/go.mod
index 923920b..c89414f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,12 @@
 module cuelang.org/go
 
 require (
+	github.com/BurntSushi/toml v0.3.1 // indirect
 	github.com/cockroachdb/apd v1.1.0
 	github.com/ghodss/yaml v1.0.0
 	github.com/google/go-cmp v0.2.0
+	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/lib/pq v1.0.0 // indirect
 	github.com/mitchellh/go-homedir v1.0.0
 	github.com/pkg/errors v0.8.0 // indirect
 	github.com/spf13/cobra v0.0.3
diff --git a/go.sum b/go.sum
index 40485fd..3d202df 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,12 @@
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -13,6 +16,10 @@
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
@@ -23,6 +30,7 @@
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -36,14 +44,11 @@
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
 github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/exp/errors v0.0.0-20181210123644-7d6377eee41f h1:B/8yFg7PHSFdahc+fMB+RUy3if9GlZmexAbcdfCwREI=
-golang.org/x/exp/errors v0.0.0-20181210123644-7d6377eee41f/go.mod h1:YgqsNsAu4fTvlab/7uiYK9LJrCIzKg/NiZUIH1/ayqo=
-golang.org/x/exp/errors v0.0.0-20181220081853-a8d4f384862a h1:juhXrq7Jv3Yw6H4azsxx0SGft00IiERvkUW6cNqnV1I=
-golang.org/x/exp/errors v0.0.0-20181220081853-a8d4f384862a/go.mod h1:YgqsNsAu4fTvlab/7uiYK9LJrCIzKg/NiZUIH1/ayqo=
 golang.org/x/exp/errors v0.0.0-20181221233300-b68661188fbf h1:4SQtY0VxhI0RZe/PFmCCfHyaPVuC5DgyXEqehsAWjwc=
 golang.org/x/exp/errors v0.0.0-20181221233300-b68661188fbf/go.mod h1:YgqsNsAu4fTvlab/7uiYK9LJrCIzKg/NiZUIH1/ayqo=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
@@ -54,6 +59,7 @@
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77 h1:s+6psEFi3o1QryeA/qyvUoVaHMCQkYVvZ0i2ZolwSJc=
 golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=