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, ¶ms{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=