cue: implement key filters using new syntax
Allow multiple "templates" (now optionalSet) which
associate a key filter with each.
The old template field is replaced with an optionals
type, which holds the collection of all optionals.
Evaluation mostly only happens at template application
type. As optionals are not involved in the picking
of disjunctions (a failure is okay), there is no
need to simplify them early, simplifying matters
greatly.
Change-Id: I81fe2722d145ba3818153c4eb8c5e7155a6efeda
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3941
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast.go b/cue/ast.go
index 582cc46..2794987 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -250,7 +250,7 @@
sig := ¶ms{}
sig.add(f, &basicType{newNode(x), stringKind})
template := &lambdaExpr{newNode(x), sig, &top{newNode(x)}}
- v1.object.addTemplate(v.ctx(), token.NoPos, template)
+ v1.object.addTemplate(v.ctx(), x.Pos(), nil, template)
case *ast.EmbedDecl:
old := v.ctx().inDefinition
@@ -277,12 +277,10 @@
v1.walk(x)
}
}
- if v.ctx().inDefinition > 0 {
+ if v.ctx().inDefinition > 0 && !obj.optionals.isFull() {
// For embeddings this is handled in binOp, in which case the
// isClosed bit is cleared if a template is introduced.
- if obj.template == nil {
- obj.closeStatus = toClose
- }
+ obj.closeStatus = toClose
}
if passDoc {
v.doc = v1.doc // signal usage of document back to parent.
@@ -388,8 +386,12 @@
} else {
f = v.label("_", true)
}
+
+ // Parse the key filter or a bulk-optional field. The special value
+ // of nil to mean "all fields".
+ var key value
if i, ok := expr.(*ast.Ident); !ok || (i.Name != "string" && i.Name != "_") {
- return v.errf(x, `only 'string' or '_' allowed in this position`)
+ key = v.walk(expr)
}
v.sel = "*"
@@ -400,7 +402,7 @@
v.setScope(n, template)
template.value = v.walk(n.Value)
- v.object.addTemplate(v.ctx(), token.NoPos, template)
+ v.object.addTemplate(v.ctx(), token.NoPos, key, template)
case *ast.TemplateLabel:
if isDef {
@@ -416,7 +418,7 @@
v.setScope(n, template)
template.value = v.walk(n.Value)
- v.object.addTemplate(v.ctx(), token.NoPos, template)
+ v.object.addTemplate(v.ctx(), token.NoPos, nil, template)
case *ast.BasicLit, *ast.Ident:
if internal.DropOptional && opt {
diff --git a/cue/ast/astutil/resolve.go b/cue/ast/astutil/resolve.go
index b6f25cb..ed9db2b 100644
--- a/cue/ast/astutil/resolve.go
+++ b/cue/ast/astutil/resolve.go
@@ -215,9 +215,10 @@
}
}
- a, ok := label.Elts[0].(*ast.Alias)
- if ok {
- // Simulate template label for now, for binding.
+ expr := label.Elts[0]
+
+ if a, ok := expr.(*ast.Alias); ok {
+ expr = a.Expr
// Add to current scope, instead of the value's, and allow
// references to bind to these illegally.
@@ -230,6 +231,7 @@
s.insert(name, a.Expr)
}
}
+ walk(s, expr)
walk(s, x.Value)
return nil
diff --git a/cue/ast_test.go b/cue/ast_test.go
index 7e893c7..7772fcb 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -224,7 +224,19 @@
C="\(a)": 5
c: C
`,
- out: `<0>{<>: <1>(ID: string)-><2>{name: <1>.ID}, foo=bar: 3, a: <0>.foo=bar, bb: 4, b1: (<0>.bb & <0>.bb), c: <0>[""+<0>.a+""]""+<0>.a+"": 5}`,
+ out: `<0>{[]: <1>(ID: string)-><2>{name: <1>.ID}, foo=bar: 3, a: <0>.foo=bar, bb: 4, b1: (<0>.bb & <0>.bb), c: <0>[""+<0>.a+""]""+<0>.a+"": 5}`,
+ }, {
+ // optional fields with key filters
+ in: `
+ JobID: =~"foo"
+ [JobID]: { name: string }
+
+ [<"s"]: { other: string }
+ `,
+ out: `<0>{` +
+ `[<0>.JobID]: <1>(_: string)-><2>{name: string}, ` +
+ `[<"s"]: <3>(_: string)-><4>{other: string}, ` +
+ `JobID: =~"foo"}`,
}, {
// illegal alias usage
in: `
@@ -263,7 +275,7 @@
v: {}
}
`,
- out: `<0>{a: <1>{<>: <2>(name: string)-><3>{n: <2>.name}, k: 1}, b: <4>{<>: <5>(X: string)-><6>{x: 0, y: 1}, v: <7>{}}}`,
+ out: `<0>{a: <1>{[]: <2>(name: string)-><3>{n: <2>.name}, k: 1}, b: <4>{[]: <5>(X: string)-><6>{x: 0, y: 1}, v: <7>{}}}`,
}, {
in: `
a: {
@@ -337,7 +349,7 @@
}}
`,
out: `<0>{` +
- `a: (<1>{d: <2>{info :: <3>{...}, Y: <2>.info.X}, <0>.base} & <4>{<>: <5>(Name: string)-><6>{info :: <7>C{X: "foo"}}, }), ` +
+ `a: (<1>{d: <2>{info :: <3>{...}, Y: <2>.info.X}, <0>.base} & <4>{[]: <5>(Name: string)-><6>{info :: <7>C{X: "foo"}}, }), ` +
`base :: <8>C{info :: <9>{...}}}`,
}, {
in: `
diff --git a/cue/binop.go b/cue/binop.go
index d2ebfb6..9c7c1fe 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -636,30 +636,12 @@
}
defer ctx.pushForwards(x, obj, y, obj).popForwards()
- tx, ex := evalLambda(ctx, x.template, x.closeStatus.shouldFinalize())
- ty, ey := evalLambda(ctx, y.template, y.closeStatus.shouldFinalize())
+ optionals, err := unifyOptionals(ctx, src, op, x, y)
+ if err != nil {
+ return err
+ }
+ obj.optionals = optionals
- var t *lambdaExpr
- switch {
- case ex != nil:
- return ex
- case ey != nil:
- return ey
- case tx != nil:
- t = tx
- case ty != nil:
- t = ty
- }
- if tx != ty && tx != nil && ty != nil {
- v := binOp(ctx, src, opUnify, tx, ty)
- if isBottom(v) {
- return v
- }
- t = v.(*lambdaExpr)
- }
- if t != nil {
- obj.template = ctx.copy(t)
- }
// If unifying with a closed struct that does not have a template,
// we need to apply the template to all elements.
@@ -680,7 +662,7 @@
break
}
}
- if !unchecked && !found && !y.allows(a.feature) && !a.definition {
+ if !unchecked && !found && !y.allows(ctx, a.feature) && !a.definition {
if a.optional {
continue
}
@@ -723,7 +705,7 @@
continue outer
}
}
- if !unchecked && !found && !x.allows(a.feature) && !a.definition {
+ if !unchecked && !found && !x.allows(ctx, a.feature) && !a.definition {
if a.optional {
continue
}
@@ -735,13 +717,67 @@
}
sort.Stable(obj)
- if unchecked && obj.template != nil {
+ if unchecked && obj.optionals.isFull() {
obj.closeStatus.unclose()
}
return obj
}
+func (x *structLit) rewriteOpt(ctx *context) (*optionals, evaluated) {
+ fn := func(v value) value {
+ if l, ok := v.(*lambdaExpr); ok {
+ l, err := evalLambda(ctx, l, x.closeStatus.shouldFinalize())
+ if err != nil {
+ return err
+ }
+ v = l
+ }
+ return ctx.copy(v)
+ }
+ c, err := x.optionals.rewrite(fn)
+ if err != nil {
+ return c, err
+ }
+ return c, nil
+}
+
+func unifyOptionals(ctx *context, src source, op op, x, y *structLit) (o *optionals, err evaluated) {
+ if x.optionals == nil && y.optionals == nil {
+ return nil, nil
+ }
+ left, err := x.rewriteOpt(ctx)
+ if err != nil {
+ return left, err
+ }
+ right, err := y.rewriteOpt(ctx)
+ if err != nil {
+ return right, err
+ }
+
+ closeStatus := x.closeStatus | y.closeStatus
+ switch {
+ case left.isDotDotDot() && right.isDotDotDot():
+
+ case left == nil && (!x.closeStatus.isClosed() || op == opUnifyUnchecked):
+ return right, nil
+
+ case right == nil && (!y.closeStatus.isClosed() || op == opUnifyUnchecked):
+ return left, nil
+
+ case op == opUnify && closeStatus.isClosed(),
+ left != nil && (left.left != nil || left.right != nil),
+ right != nil && (right.left != nil || right.right != nil):
+ return &optionals{closeStatus, op, left, right, nil}, nil
+ }
+
+ // opUnify where both structs are open or opUnifyUnchecked
+ for _, f := range right.fields {
+ left.add(ctx, f.key, f.value)
+ }
+ return left, nil
+}
+
func (x *nullLit) binOp(ctx *context, src source, op op, other evaluated) evaluated {
// TODO: consider using binSrc instead of src.base() for better traceability.
switch other.(type) {
diff --git a/cue/copy.go b/cue/copy.go
index 6c80735..fff4342 100644
--- a/cue/copy.go
+++ b/cue/copy.go
@@ -41,15 +41,12 @@
}
obj.emit = emit
- t := x.template
- if t != nil {
- v := ctx.copy(t)
- if isBottom(v) {
- return t, false
- }
- t = v
+ fn := func(v value) value { return ctx.copy(v) }
+ o, err := x.optionals.rewrite(fn)
+ if err != nil {
+ return err, false
}
- obj.template = t
+ obj.optionals = o
for i, a := range x.arcs {
a.setValue(ctx.copy(a.v))
diff --git a/cue/debug.go b/cue/debug.go
index 3742770..e855f7f 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -276,6 +276,46 @@
p.str(x.value)
write(")")
+ case *optionals:
+ if x == nil {
+ break
+ }
+ wrap := func(v *optionals) {
+ if x.closed.isClosed() {
+ write("C{")
+ }
+ p.str(v)
+ if x.closed.isClosed() {
+ write("}")
+ }
+ }
+ switch {
+ case x.op == opUnify:
+ write("(")
+ wrap(x.left)
+ write(" & ")
+ wrap(x.right)
+ write(")")
+
+ case x.op == opUnifyUnchecked:
+ wrap(x.left)
+ write(", ")
+ wrap(x.right)
+
+ default:
+ for i, t := range x.fields {
+ if i > 0 {
+ write(", ")
+ }
+ write("[")
+ if t.key != nil {
+ p.str(t.key)
+ }
+ write("]: ")
+ p.str(t.value)
+ }
+ }
+
case *structLit:
if x == nil {
write("*nil node*")
@@ -288,19 +328,12 @@
write("C")
}
write("{")
- topDefault := false
- switch {
- case x.template != nil:
- lambda, ok := x.template.(*lambdaExpr)
- if ok {
- if _, topDefault = lambda.value.(*top); topDefault {
- break
- }
- }
- write("<>: ")
- p.str(x.template)
+ topDefault := x.optionals.isDotDotDot()
+ if !topDefault && x.optionals != nil {
+ p.str(x.optionals)
write(", ")
}
+
if x.emit != nil {
p.str(x.emit)
write(", ")
diff --git a/cue/export.go b/cue/export.go
index ae5af74..f9aee37 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -200,12 +200,15 @@
return short
}
-func mkTemplate(n *ast.Ident) ast.Label {
- var expr ast.Expr = n
- switch n.Name {
- case "":
+func (p *exporter) mkTemplate(v value, n *ast.Ident) ast.Label {
+ var expr ast.Expr
+ if v != nil {
+ expr = p.expr(v)
+ } else {
expr = ast.NewIdent("_")
- case "_":
+ }
+ switch n.Name {
+ case "", "_":
default:
expr = &ast.Alias{Ident: n, Expr: ast.NewIdent("_")}
}
@@ -225,8 +228,20 @@
return false
}
}
- if _, ok := label.(*ast.ListLit); ok {
- return true
+ if l, ok := label.(*ast.ListLit); ok {
+ if len(l.Elts) != 1 {
+ return false
+ }
+ expr := l.Elts[0]
+ if a, ok := expr.(*ast.Alias); ok {
+ expr = a.Expr
+ }
+ if i, ok := expr.(*ast.Ident); ok {
+ if i.Name == "_" || i.Name == "string" {
+ return true
+ }
+ }
+ return false
}
}
}
@@ -243,7 +258,7 @@
if !p.showOptional() {
return s
}
- if isClosed && !p.inDef {
+ if isClosed && !p.inDef && !hasTemplate(s) {
return ast.NewCall(ast.NewIdent("close"), s)
}
if !isClosed && p.inDef && !hasTemplate(s) {
@@ -477,24 +492,11 @@
// optional fields is requested. If a struct is not closed it was
// already generated before. Furthermore, if if we are in evaluation
// mode, the struct is already unified, so there is no need to print it.
- case x.template != nil && p.showOptional() && p.isClosed(x) && !doEval(p.mode):
- l, ok := x.template.evalPartial(p.ctx).(*lambdaExpr)
- if !ok {
+ case p.showOptional() && p.isClosed(x) && !doEval(p.mode):
+ if x.optionals == nil {
break
}
- v := l.value
- if c, ok := v.(*closeIfStruct); ok {
- v = c.value
- }
- if _, ok := v.(*top); ok {
- break
- }
- expr = &ast.BinaryExpr{X: expr, Op: token.AND, Y: &ast.StructLit{
- Elts: []ast.Decl{&ast.Field{
- Label: mkTemplate(p.identifier(l.params.arcs[0].feature)),
- Value: p.expr(l.value),
- }},
- }}
+ p.optionals(st, x.optionals)
}
return expr
@@ -668,6 +670,61 @@
}
}
+func (p *exporter) optionalsExpr(x *optionals, isClosed bool) ast.Expr {
+ st := &ast.StructLit{}
+ // An empty struct has meaning in case of closed structs, where they
+ // indicate no other fields may be added. Non-closed empty structs should
+ // have been optimized away. In case they are not, it is just a no-op.
+ if x != nil {
+ p.optionals(st, x)
+ }
+ if isClosed {
+ return ast.NewCall(ast.NewIdent("close"), st)
+ }
+ return st
+}
+
+func (p *exporter) optionals(st *ast.StructLit, x *optionals) {
+ switch x.op {
+ default:
+ for _, t := range x.fields {
+ l, ok := t.value.evalPartial(p.ctx).(*lambdaExpr)
+ if !ok {
+ // Really should not happen.
+ continue
+ }
+ v := l.value
+ if c, ok := v.(*closeIfStruct); ok {
+ v = c.value
+ }
+ st.Elts = append(st.Elts, &ast.Field{
+ Label: p.mkTemplate(t.key, p.identifier(l.params.arcs[0].feature)),
+ Value: p.expr(l.value),
+ })
+ }
+
+ case opUnify:
+ // Optional constraints added with normal unification are embedded as an
+ // expression. This relies on the fact that a struct embedding a closed
+ // struct will itself be closed.
+ st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: &ast.BinaryExpr{
+ X: p.optionalsExpr(x.left, x.left.isClosed()),
+ Op: token.AND,
+ Y: p.optionalsExpr(x.right, x.right.isClosed()),
+ }})
+
+ case opUnifyUnchecked:
+ // Constraints added with unchecked unification are embedded
+ // individually. It doesn't matter here whether this originated from
+ // regular unification of open structs or embedded closed structs.
+ // The result in each case is unchecked unification.
+ left := p.optionalsExpr(x.left, false)
+ right := p.optionalsExpr(x.right, false)
+ st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: left})
+ st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: right})
+ }
+}
+
func (p *exporter) structure(x *structLit, addTempl bool) (ret *ast.StructLit, err *bottom) {
obj := &ast.StructLit{}
if doEval(p.mode) {
@@ -688,18 +745,11 @@
if x.emit != nil {
obj.Elts = append(obj.Elts, &ast.EmbedDecl{Expr: p.expr(x.emit)})
}
- switch {
- case x.template != nil && p.showOptional() && addTempl:
- l, ok := x.template.evalPartial(p.ctx).(*lambdaExpr)
- if ok {
- if _, ok := l.value.(*top); ok && !p.isClosed(x) {
- break
- }
- obj.Elts = append(obj.Elts, &ast.Field{
- Label: mkTemplate(p.identifier(l.params.arcs[0].feature)),
- Value: p.expr(l.value),
- })
- } // TODO: else record error
+ if p.showOptional() && x.optionals != nil &&
+ // Optional field constraints may be omitted if they were already
+ // applied and no more new fields may be added.
+ !(doEval(p.mode) && x.optionals.isEmpty() && p.isClosed(x)) {
+ p.optionals(obj, x.optionals)
}
for i, a := range x.arcs {
f := &ast.Field{
diff --git a/cue/export_test.go b/cue/export_test.go
index 302b1a9..eefbfa2 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -783,11 +783,13 @@
[_]: int64
}
X :: {
- x: int64
+ [_]: int64
+ x: int64
}
- x: close({
- x: int64
- })
+ x: {
+ [_]: int64
+ x: int64
+ }
}`),
}, {
eval: true,
@@ -872,6 +874,42 @@
b?: 2
c: 3
}`),
+ }, {
+ eval: true,
+ in: `
+ A :: {
+ [=~"^[a-s]*$"]: int
+ }
+ B :: {
+ [=~"^[m-z]+"]: int
+ }
+ C: {A & B}
+ D :: {A & B}
+ `,
+ // TODO: the outer close of C could be optimized away.
+ out: unindent(`
+ {
+ A :: {
+ [=~"^[a-s]*$"]: int
+ }
+ B :: {
+ [=~"^[m-z]+"]: int
+ }
+ C: close({
+ close({
+ [=~"^[a-s]*$"]: int
+ }) & close({
+ [=~"^[m-z]+"]: int
+ })
+ })
+ D :: {
+ close({
+ [=~"^[a-s]*$"]: int
+ }) & close({
+ [=~"^[m-z]+"]: int
+ })
+ }
+ }`),
}}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
diff --git a/cue/go.go b/cue/go.go
index 9c18208..93a8caf 100644
--- a/cue/go.go
+++ b/cue/go.go
@@ -642,7 +642,7 @@
if isBottom(v) {
return v
}
- obj.template = &lambdaExpr{params: sig, value: v}
+ obj.optionals = newOptional(nil, &lambdaExpr{params: sig, value: v})
e = wrapOrNull(obj)
}
diff --git a/cue/go_test.go b/cue/go_test.go
index d0411b4..b9d749e 100644
--- a/cue/go_test.go
+++ b/cue/go_test.go
@@ -212,9 +212,9 @@
}, {
map[string]struct{ A map[string]uint }{},
`(*null | ` +
- `<0>{<>: <1>(_: string)-><2>{` +
+ `<0>{[]: <1>(_: string)-><2>{` +
`A?: (*null | ` +
- `<3>{<>: <4>(_: string)->(int & >=0 & int & <=18446744073709551615), })}, })`,
+ `<3>{[]: <4>(_: string)->(int & >=0 & int & <=18446744073709551615), })}, })`,
}, {
map[float32]int{},
`_|_(unsupported Go type for map key (float32))`,
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 4f4d627..d913702 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -1188,8 +1188,8 @@
`Foo1 :: <3>C{field: int, field2: string}, ` +
`foo: _|_(2:field "feild" not allowed in closed struct), ` +
`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}, ` +
+ `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)}`,
}, {
@@ -1269,12 +1269,12 @@
}
`,
out: `<0>{` +
- `S :: <1>{<>: <2>(_: string)-><3>C{a: int}, }, ` +
- `a: <4>{<>: <5>(_: string)-><6>C{a: int}, v: _|_(int:field "b" not allowed in closed struct)}, ` +
- `b: <7>{<>: <8>(_: string)->(<9>C{a: int} | <10>C{b: int}), w: _|_(int:empty disjunction: field "c" not allowed in closed struct)}, ` +
- `Q :: <11>{<>: <12>(_: string)->(<13>C{a: int} | <14>C{b: int}), }, ` +
- `c: <15>{<>: <16>(_: string)->[<17>C{a: int},<18>C{b: int}], w: [_|_(int:field "d" not allowed in closed struct),<19>C{b: int}]}, ` +
- `R :: <20>{<>: <21>(_: string)->[<22>C{a: int},<23>C{b: int}], }}`,
+ `S :: <1>{[]: <2>(_: string)-><3>C{a: int}, }, ` +
+ `a: <4>{[]: <5>(_: string)-><6>C{a: int}, v: _|_(int:field "b" not allowed in closed struct)}, ` +
+ `b: <7>{[]: <8>(_: string)->(<9>C{a: int} | <10>C{b: int}), w: _|_(int:empty disjunction: field "c" not allowed in closed struct)}, ` +
+ `Q :: <11>{[]: <12>(_: string)->(<13>C{a: int} | <14>C{b: int}), }, ` +
+ `c: <15>{[]: <16>(_: string)->[<17>C{a: int},<18>C{b: int}], w: [_|_(int:field "d" not allowed in closed struct),<19>C{b: int}]}, ` +
+ `R :: <20>{[]: <21>(_: string)->[<22>C{a: int},<23>C{b: int}], }}`,
}, {
desc: "definitions with disjunctions",
in: `
@@ -1547,7 +1547,7 @@
}
t x: { b str: "DDDD" }
`,
- out: `<0>{res: [<1>{<>: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <5>{a: "XDDDD", c: "X", b: <6>{str: "DDDD"}}}], t: <7>{<>: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <8>{a: "XDDDD", c: "X", b: <9>{str: "DDDD"}}}}`,
+ out: `<0>{res: [<1>{[]: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <5>{a: "XDDDD", c: "X", b: <6>{str: "DDDD"}}}], t: <7>{[]: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <8>{a: "XDDDD", c: "X", b: <9>{str: "DDDD"}}}}`,
}, {
desc: "interpolation",
in: `
@@ -1644,14 +1644,14 @@
v: {}
w: { x: 0 }
}
- b: { <y>: {} } // TODO: allow different name
+ b: { <y>: {} }
c: {
<Name>: { name: Name, y: 1 }
foo: {}
bar: _
}
`,
- out: `<0>{a: <1>{<>: <2>(name: string)->int, k: 1}, b: <3>{<>: <4>(X: string)->(<5>{x: 0, y: (*1 | int)} & <6>{}), v: <7>{x: 0, y: (*1 | int)}, w: <8>{x: 0, y: (*1 | int)}}, c: <9>{<>: <10>(Name: string)-><11>{name: <10>.Name, y: 1}, foo: <12>{name: "foo", y: 1}, bar: <13>{name: "bar", y: 1}}}`,
+ out: `<0>{a: <1>{[]: <2>(name: string)->int, k: 1}, b: <3>{[]: <4>(X: string)->(<5>{x: 0, y: (*1 | int)} & <6>{}), v: <7>{x: 0, y: (*1 | int)}, w: <8>{x: 0, y: (*1 | int)}}, c: <9>{[]: <10>(Name: string)-><11>{name: <10>.Name, y: 1}, foo: <12>{name: "foo", y: 1}, bar: <13>{name: "bar", y: 1}}}`,
}, {
desc: "range unification",
in: `
@@ -1768,7 +1768,7 @@
"\(k)": v
}
`,
- out: `<0>{obj: <1>{<>: <2>(Name: string)-><3>{a: (*"dummy" | string) if true yield <4>{sub: <5>{as: <3>.a}}}, foo: <6>{a: "bar", sub: <7>{as: "bar"}}}, reg: 4}`,
+ out: `<0>{obj: <1>{[]: <2>(Name: string)-><3>{a: (*"dummy" | string) if true yield <4>{sub: <5>{as: <3>.a}}}, foo: <6>{a: "bar", sub: <7>{as: "bar"}}}, reg: 4}`,
}, {
desc: "builtins",
in: `
@@ -2032,7 +2032,7 @@
service bar: { port: 8000 }
service baz: { name: "foobar" }
`,
- out: `<0>{service: <1>{<>: <2>(Name: string)-><3>{name: (string | *<2>.Name), port: (int | *7080)}, foo: <4>{name: "foo", port: 7080}, bar: <5>{name: "bar", port: 8000}, baz: <6>{name: "foobar", port: 7080}}}`,
+ out: `<0>{service: <1>{[]: <2>(Name: string)-><3>{name: (string | *<2>.Name), port: (int | *7080)}, foo: <4>{name: "foo", port: 7080}, bar: <5>{name: "bar", port: 8000}, baz: <6>{name: "foobar", port: 7080}}}`,
}, {
desc: "field templates",
in: `
@@ -2052,7 +2052,7 @@
bar: _
}
`,
- out: `<0>{a: <1>{<>: <2>(name: string)->int, k: 1}, b: <3>{<>: <4>(X: string)->(<5>{x: 0, y: (*1 | int)} & <6>{}), v: <7>{x: 0, y: 1}, w: <8>{x: 0, y: 0}}, c: <9>{<>: <10>(Name: string)-><11>{name: <10>.Name, y: 1}, foo: <12>{name: "foo", y: 1}, bar: <13>{name: "bar", y: 1}}}`,
+ out: `<0>{a: <1>{[]: <2>(name: string)->int, k: 1}, b: <3>{[]: <4>(X: string)->(<5>{x: 0, y: (*1 | int)} & <6>{}), v: <7>{x: 0, y: 1}, w: <8>{x: 0, y: 0}}, c: <9>{[]: <10>(Name: string)-><11>{name: <10>.Name, y: 1}, foo: <12>{name: "foo", y: 1}, bar: <13>{name: "bar", y: 1}}}`,
}, {
desc: "field comprehension",
in: `
@@ -2120,7 +2120,7 @@
a <Name>: { name: Name }
a foo: {}
`,
- out: `<0>{a: <1>{<>: <2>(X: string)->(<3>{name: <2>.X} & <4>{name: <2>.X}), foo: <5>{name: "foo"}}}`,
+ out: `<0>{a: <1>{[]: <2>(X: string)->(<3>{name: <2>.X} & <4>{name: <2>.X}), foo: <5>{name: "foo"}}}`,
}, {
// TODO: rename EE and FF to E and F to check correct ordering.
@@ -2134,12 +2134,12 @@
a "C" b "D": _
a "EE" b "FF": { c: "bar" }
`,
- out: `<0>{a: <1>{<>: <2>(A: string)-><3>{b: <4>{<>: <5>(B: string)-><6>{name: <2>.A, kind: <5>.B}, }}, ` +
- `A: <7>{b: <8>{<>: <9>(B: string)-><10>{name: <11>.A, kind: <9>.B}, ` +
+ out: `<0>{a: <1>{[]: <2>(A: string)-><3>{b: <4>{[]: <5>(B: string)-><6>{name: <2>.A, kind: <5>.B}, }}, ` +
+ `A: <7>{b: <8>{[]: <9>(B: string)-><10>{name: <11>.A, kind: <9>.B}, ` +
`B: <12>{name: "A", kind: "B"}}}, ` +
- `C: <13>{b: <14>{<>: <15>(B: string)-><16>{name: <17>.A, kind: <15>.B}, ` +
+ `C: <13>{b: <14>{[]: <15>(B: string)-><16>{name: <17>.A, kind: <15>.B}, ` +
`D: <18>{name: "C", kind: "D"}}}, ` +
- `EE: <19>{b: <20>{<>: <21>(B: string)-><22>{name: <23>.A, kind: <21>.B}, ` +
+ `EE: <19>{b: <20>{[]: <21>(B: string)-><22>{name: <23>.A, kind: <21>.B}, ` +
`FF: <24>{name: "EE", kind: "FF", c: "bar"}}}}}`,
}, {
desc: "template unification within one struct",
@@ -2152,7 +2152,7 @@
a "C": _
a "E": { c: "bar" }
`,
- out: `<0>{a: <1>{<>: <2>(A: string)->(<3>{name: <2>.A} & <4>{kind: <2>.A}), ` +
+ out: `<0>{a: <1>{[]: <2>(A: string)->(<3>{name: <2>.A} & <4>{kind: <2>.A}), ` +
`E: <5>{name: "E", kind: "E", c: "bar"}, ` +
`A: <6>{name: "A", kind: "A"}, ` +
`C: <7>{name: "C", kind: "C"}}}`,
@@ -2204,8 +2204,8 @@
}
a b c d: "bar"
`,
- out: `<0>{num: 1, a: <1>{<>: <2>(A: string)-><3>{<>: <4>(B: string)-><5>{name: <2>.A, kind: <4>.B}, }, ` +
- `b: <6>{<>: <7>(B: string)-><8>{name: <9>.A, kind: <7>.B}, ` +
+ out: `<0>{num: 1, a: <1>{[]: <2>(A: string)-><3>{[]: <4>(B: string)-><5>{name: <2>.A, kind: <4>.B}, }, ` +
+ `b: <6>{[]: <7>(B: string)-><8>{name: <9>.A, kind: <7>.B}, ` +
`c: <10>{name: "b", kind: "c", ` +
`d: "bar"}}}}`,
}, {
@@ -2271,7 +2271,7 @@
`<3>{name: "foobar", type: "service", port: 7080}], ` +
`service: <4>{` +
- `<>: <5>(Name: string)-><6>{name: (*<5>.Name | string), type: "service", port: (*7080 | int)}, ` +
+ `[]: <5>(Name: string)-><6>{name: (*<5>.Name | string), type: "service", port: (*7080 | int)}, ` +
`foo: <7>{name: "foo", type: "service", port: 7080}, ` +
`bar: <8>{name: "bar", type: "service", port: 8000}, ` +
`baz: <9>{name: "foobar", type: "service", port: 7080}}}`,
@@ -2335,7 +2335,7 @@
`,
// TODO(perf): unification should catch shared node.
out: `<0>{res: [<1>{d: "b", s: "ab"}], ` +
- `a: <2>{b: <3>{<>: <4>(C: string)-><5>{d: string, s: ("a" + <5>.d)}, c: <6>{d: string, s: ("a" + <7>.d)}}}}`,
+ `a: <2>{b: <3>{[]: <4>(C: string)-><5>{d: string, s: ("a" + <5>.d)}, c: <6>{d: string, s: ("a" + <7>.d)}}}}`,
}, {
desc: "complex groundness 2",
in: `
@@ -2347,7 +2347,7 @@
a b <C>: { d: string, s: "a" + d }
a b c d: string
`,
- out: `<0>{r1: <1>{y: "c", res: <2>{d: "c", s: "ac"}}, f1: <3>{y: string, res: <4>{d: string, s: (("a" + <5>.d) & ("a" + <5>.d))}}, a: <6>{b: <7>{<>: <8>(C: string)-><9>{d: string, s: ("a" + <9>.d)}, c: <10>{d: string, s: (("a" + <11>.d) & ("a" + <11>.d))}}}}`,
+ out: `<0>{r1: <1>{y: "c", res: <2>{d: "c", s: "ac"}}, f1: <3>{y: string, res: <4>{d: string, s: (("a" + <5>.d) & ("a" + <5>.d))}}, a: <6>{b: <7>{[]: <8>(C: string)-><9>{d: string, s: ("a" + <9>.d)}, c: <10>{d: string, s: (("a" + <11>.d) & ("a" + <11>.d))}}}}`,
}, {
desc: "references from template to concrete",
in: `
@@ -2359,8 +2359,8 @@
}
t x: { b str: "DDDD" }
`,
- out: `<0>{res: [<1>{<>: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <5>{a: "XDDDD", c: "X", b: <6>{str: "DDDD"}}}], ` +
- `t: <7>{<>: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <8>{a: "XDDDD", c: "X", b: <9>{str: "DDDD"}}}}`,
+ out: `<0>{res: [<1>{[]: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <5>{a: "XDDDD", c: "X", b: <6>{str: "DDDD"}}}], ` +
+ `t: <7>{[]: <2>(X: string)-><3>{a: (<3>.c + <3>.b.str), c: "X", b: <4>{str: string}}, x: <8>{a: "XDDDD", c: "X", b: <9>{str: "DDDD"}}}}`,
}, {
// TODO: A nice property for CUE to have would be that evaluation time
// is proportional to the number of output nodes (note that this is
@@ -2549,7 +2549,7 @@
X: "foo"
}}
`,
- out: `<0>{a: <1>{<>: <2>(Name: string)-><3>{info :: <4>C{X: "foo"}}, d: <5>C{info :: <6>C{X: "foo"}, Y: "foo"}}, base :: <7>C{info :: <8>{...}}}`,
+ out: `<0>{a: <1>{[]: <2>(Name: string)-><3>{info :: <4>C{X: "foo"}}, d: <5>C{info :: <6>C{X: "foo"}, Y: "foo"}}, base :: <7>C{info :: <8>{...}}}`,
}, {
desc: "comparison against bottom",
in: `
@@ -2579,7 +2579,7 @@
}
}
`,
- out: `<0>{Workflow :: <1>C{jobs: <2>{<>: <3>(jobID: string)-><4>C{}, }, JobID :: or ([ <5>for k, _ in <6>.jobs yield <5>.k ])}, foo: <7>C{jobs: <8>{<>: <9>(jobID: string)-><10>C{}, foo: <11>C{}}, JobID :: "foo"}}`,
+ out: `<0>{Workflow :: <1>C{jobs: <2>{[]: <3>(jobID: string)-><4>C{}, }, JobID :: or ([ <5>for k, _ in <6>.jobs yield <5>.k ])}, foo: <7>C{jobs: <8>{[]: <9>(jobID: string)-><10>C{}, foo: <11>C{}}, JobID :: "foo"}}`,
}, {
desc: "Issue #153",
in: `
@@ -2605,6 +2605,7 @@
`,
out: `<0>{<1>{listOfCloseds: [_|_(2:field "b" not allowed in closed struct)]}, Foo: <2>{listOfCloseds: []}, Closed :: <3>C{a: 0}, Junk: <4>{b: 2}}`,
}, {
+ desc: "label and field aliases",
in: `
p: [ID=string]: { name: ID }
A="foo=bar": "str"
@@ -2616,13 +2617,122 @@
c: C
`,
out: `<0>{` +
- `p: <1>{<>: <2>(ID: string)-><3>{name: <2>.ID}, }, ` +
+ `p: <1>{[]: <2>(ID: string)-><3>{name: <2>.ID}, }, ` +
`foo=bar: "str", ` +
`a: "str", ` +
`bb: 4, ` +
`b1: 4, ` +
`c: 5, ` +
`str: 5}`,
+ }, {
+ desc: "optionals with label fiters",
+ in: `
+ JobID :: =~"^[a-zA-Z]*$"
+ Job :: {
+ name: string
+ cmd: string
+ }
+ Jobs :: {
+ [JobID]: Job
+ [=~"Test$"]: name: =~"^test" // Must work without ...
+ }
+
+ jobs: foo: name: "allGood"
+ jobs: foo: name: "allGood"
+
+ jobs1: Jobs
+ jobs1: foo1: {} // faulty
+
+ jobs2: Jobs
+ jobs2: fooTest: name: "badName" // faulty
+
+ jobs3: Jobs
+ jobs3: [string]: Job
+ jobs3: fooTest1: name: "badName" // faulty
+ `,
+ out: `<0>{` +
+ `JobID :: =~"^[a-zA-Z]*$", ` +
+ `Job :: <1>C{name: string, cmd: string}, ` +
+ `Jobs :: <2>C{[=~"^[a-zA-Z]*$"]: <3>(_: string)-><4>.Job, [=~"Test$"]: <5>(_: string)-><6>C{name: =~"^test"}, }, ` +
+ `jobs: <7>{foo: <8>{name: "allGood"}}, ` +
+ `jobs1: _|_(<9>{}:field "foo1" not allowed in closed struct), ` +
+ `jobs2: <10>C{[=~"^[a-zA-Z]*$"]: <11>(_: string)-><4>.Job, [=~"Test$"]: <12>(_: string)-><13>C{name: =~"^test"}, fooTest: _|_(string:field "cmd" not allowed in closed struct)}, ` +
+ `jobs3: _|_(<14>{name: "badName"}:field "fooTest1" not allowed in closed struct)}`,
+ }, {
+ desc: "optionals in open structs",
+ in: `
+ A: {
+ [=~"^[a-s]*$"]: int
+ }
+ B: {
+ [=~"^[m-z]*$"]: int
+ }
+ C :: {A & B}
+ c: C & { aaa: 3 }
+ `,
+ out: `<0>{A: <1>{[=~"^[a-s]*$"]: <2>(_: string)->int, }, B: <3>{[=~"^[m-z]*$"]: <4>(_: string)->int, }, C :: <5>C{[=~"^[a-s]*$"]: <6>(_: string)->int, [=~"^[m-z]*$"]: <7>(_: string)->int, }, c: <8>C{[=~"^[a-s]*$"]: <9>(_: string)->int, [=~"^[m-z]*$"]: <10>(_: string)->int, aaa: 3}}`,
+ }, {
+ desc: "conjunction of optional sets",
+ in: `
+ A :: {
+ [=~"^[a-s]*$"]: int
+ }
+ B :: {
+ [=~"^[m-z]*$"]: int
+ }
+
+ C :: A & B
+ c: C & { aaa: 3 }
+
+ D :: {A & B}
+ d: D & { aaa: 3 }
+ `,
+ out: `<0>{` +
+ `A :: <1>C{[=~"^[a-s]*$"]: <2>(_: string)->int, }, ` +
+ `B :: <3>C{[=~"^[m-z]*$"]: <4>(_: string)->int, }, ` +
+ `C :: <5>C{(C{[=~"^[a-s]*$"]: <6>(_: string)->int} & C{[=~"^[m-z]*$"]: <7>(_: string)->int}), }, ` +
+ `c: _|_(3:field "aaa" not allowed in closed struct), ` +
+ `D :: <8>C{(C{[=~"^[a-s]*$"]: <9>(_: string)->int} & C{[=~"^[m-z]*$"]: <10>(_: string)->int}), }, ` +
+ `d: _|_(3:field "aaa" not allowed in closed struct)` +
+ `}`,
+ }, {
+ desc: "continue recursive closing for optionals",
+ in: `
+ S :: {
+ [string]: { a: int }
+ }
+ a: S & {
+ v: { b: int }
+ }
+ `,
+ out: `<0>{S :: <1>{[]: <2>(_: string)-><3>C{a: int}, }, a: <4>{[]: <5>(_: string)-><6>C{a: int}, v: _|_(int:field "b" not allowed in closed struct)}}`,
+ }, {
+ desc: "augment closed optionals",
+ in: `
+ A :: {
+ [=~"^[a-s]*$"]: int
+ }
+ B :: {
+ [=~"^[m-z]*?"]: int
+ }
+ C :: {
+ A & B
+ [=~"^Q*$"]: int
+ }
+ c: C & { QQ: 3 }
+ D :: {
+ A
+ B
+ }
+ d: D & { aaa: 4 }
+ `,
+ out: `<0>{` +
+ `A :: <1>C{[=~"^[a-s]*$"]: <2>(_: string)->int, }, ` +
+ `B :: <3>C{[=~"^[m-z]*?"]: <4>(_: string)->int, }, ` +
+ `C :: <5>C{C{[=~"^Q*$"]: <6>(_: string)->int}, C{(C{[=~"^[a-s]*$"]: <7>(_: string)->int} & C{[=~"^[m-z]*?"]: <8>(_: string)->int})}, }, ` +
+ `c: <9>C{C{[=~"^Q*$"]: <10>(_: string)->int}, C{(C{[=~"^[a-s]*$"]: <11>(_: string)->int} & C{[=~"^[m-z]*?"]: <12>(_: string)->int})}, QQ: 3}, ` +
+ `D :: <13>C{[=~"^[a-s]*$"]: <14>(_: string)->int, [=~"^[m-z]*?"]: <15>(_: string)->int, }, ` +
+ `d: <16>C{[=~"^[a-s]*$"]: <17>(_: string)->int, [=~"^[m-z]*?"]: <18>(_: string)->int, aaa: 4}}`,
}}
rewriteHelper(t, testCases, evalFull)
}
diff --git a/cue/rewrite_test.go b/cue/rewrite_test.go
index bbf3288..f8e9d1a 100644
--- a/cue/rewrite_test.go
+++ b/cue/rewrite_test.go
@@ -66,13 +66,12 @@
a.setValue(rewriteRec(ctx, a.v, v, m))
arcs[i] = a
}
- t := x.template
- if t != nil {
- v := rewriteRec(ctx, t, t.evalPartial(ctx), m)
- if isBottom(v) {
- return v
- }
- t = v
+
+ t, e := x.optionals.rewrite(func(v value) value {
+ return rewriteRec(ctx, v, v.evalPartial(ctx), m)
+ })
+ if e != nil {
+ return err
}
emit := testResolve(ctx, x.emit, m)
obj := &structLit{x.baseValue, emit, t, x.closeStatus, x.comprehensions, arcs, nil}
diff --git a/cue/types.go b/cue/types.go
index 581f286..7881b55 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -989,18 +989,11 @@
ctx := v.ctx()
switch x := v.path.v.(type) {
case *structLit:
- if x.template == nil {
+ t, _ := x.optionals.constraint(ctx, nil)
+ if t == nil {
break
}
- fn, ok := ctx.manifest(x.template).(*lambdaExpr)
- if !ok {
- // TODO: return an error instead.
- break
- }
- // Note, this template does not maintain the relation between the
- // the attribute value and the instance.
- y := fn.call(ctx, x, &basicType{x.baseValue, stringKind})
- return newValueRoot(ctx, y), true
+ return newValueRoot(ctx, t), true
case *list:
return newValueRoot(ctx, x.typ), true
}
@@ -1151,7 +1144,7 @@
obj = &structLit{
obj.baseValue, // baseValue
obj.emit, // emit
- obj.template, // template
+ obj.optionals, // template
obj.closeStatus, // closeStatus
nil, // comprehensions
arcs, // arcs
@@ -1281,17 +1274,17 @@
func (v Value) Template() func(label string) Value {
ctx := v.ctx()
x, ok := v.path.cache.(*structLit)
- if !ok || x.template == nil {
+ if !ok || x.optionals.isEmpty() {
return nil
}
- fn, ok := ctx.manifest(x.template).(*lambdaExpr)
- if !ok {
- return nil
- }
- return func(label string) Value {
+
+ return func(label string) (v Value) {
arg := &stringLit{x.baseValue, label, nil}
- y := fn.call(ctx, x, arg)
- return newValueRoot(ctx, y)
+
+ if v, _ := x.optionals.constraint(ctx, arg); v != nil {
+ return newValueRoot(ctx, v)
+ }
+ return v
}
}
diff --git a/cue/value.go b/cue/value.go
index 17a8ec2..27aeb7e 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -637,9 +637,9 @@
// templates [][]value
// catch_all: value
- // template must evaluated to a lambda and is applied to all concrete
- // values in the struct, whether it be open or closed.
- template value
+ // optionals holds pattern-constraint pairs that
+ // are applied to all concrete values in this struct.
+ optionals *optionals
closeStatus closeMode
comprehensions []value
@@ -649,6 +649,254 @@
expanded evaluated
}
+// optionals holds a set of key pattern-constraint pairs, where constraints are
+// to be applied to concrete fields of which the label matches the key pattern.
+//
+// optionals will either hold concrete fields or a couple of nested optional
+// structs combined based on the op type, but not both.
+type optionals struct {
+ closed closeMode
+ op op
+ left *optionals // nil means empty closed struct
+ right *optionals // nil means empty closed struct
+ fields []optionalSet
+}
+
+type optionalSet struct {
+ // A key filter may be nil, in which case it means all strings, or _.
+ key value
+
+ // constraint must evaluate to a lambda and is applied to any concrete
+ // value for which the key matches key.
+ value value
+}
+
+func newOptional(key, value value) *optionals {
+ return &optionals{
+ fields: []optionalSet{{key, value}},
+ }
+}
+
+// isClosed mirrors the closed status of the struct to which
+// this optionals belongs.
+func (o *optionals) isClosed() bool {
+ if o == nil {
+ return true
+ }
+ return o.closed.isClosed()
+}
+
+func (o *optionals) close() *optionals {
+ if o == nil {
+ return nil
+ }
+ o.closed |= isClosed
+ return o
+}
+
+// isEmpty reports whether this optionals may report true for match. Even if an
+// optionals is empty, it may still hold constraints to be applied to already
+// existing concrete fields.
+func (o *optionals) isEmpty() bool {
+ if o == nil {
+ return true
+ }
+ le := o.left.isEmpty()
+ re := o.right.isEmpty()
+
+ if o.op == opUnify {
+ if le && o.left.isClosed() {
+ return true
+ }
+ if re && o.right.isClosed() {
+ return true
+ }
+ }
+ return le && re && len(o.fields) == 0
+}
+
+// isFull reports whether match reports true for all fields.
+func (o *optionals) isFull() bool {
+ found, _ := o.match(nil, nil)
+ return found
+}
+
+// match reports whether a field with the given name may be added in the
+// associated struct as a new field. ok is false, fif there was any closed
+// struct that failed to match. Even if match returns false, there may still be
+// constraints represented by optionals that are to be applied to existing
+// concrete fields.
+func (o *optionals) match(ctx *context, str *stringLit) (found, ok bool) {
+ if o == nil {
+ return false, true
+ }
+
+ found1, ok := o.left.match(ctx, str)
+ if !ok && o.op == opUnify {
+ return false, false
+ }
+
+ found2, ok := o.right.match(ctx, str)
+ if !ok && o.op == opUnify {
+ return false, false
+ }
+
+ if found1 || found2 {
+ return true, true
+ }
+
+ for _, f := range o.fields {
+ if f.key == nil {
+ return true, true
+ }
+ if str != nil {
+ v := binOp(ctx, f.value, opUnify, f.key.evalPartial(ctx), str)
+ if !isBottom(v) {
+ return true, true
+ }
+ }
+ }
+
+ return false, !o.closed.isClosed()
+}
+
+func (o *optionals) allows(ctx *context, f label) bool {
+ if o == nil {
+ return false
+ }
+
+ str := ctx.labelStr(f)
+ arg := &stringLit{str: str}
+
+ found, ok := o.match(ctx, arg)
+ return found && ok
+}
+
+func (o *optionals) add(ctx *context, key, value value) {
+ for i, b := range o.fields {
+ if b.key == key {
+ o.fields[i].value = mkBin(ctx, token.NoPos, opUnify, b.value, value)
+ return
+ }
+ }
+ o.fields = append(o.fields, optionalSet{key, value})
+}
+
+// isDotDotDot reports whether optionals only contains fully-qualified
+// constraints. This is useful for some optimizations.
+func (o *optionals) isDotDotDot() bool {
+ if o == nil {
+ return false
+ }
+ if len(o.fields) > 1 {
+ return false
+ }
+ if len(o.fields) == 1 {
+ f := o.fields[0]
+ if f.key != nil {
+ return false
+ }
+ lambda, ok := f.value.(*lambdaExpr)
+ if ok {
+ if _, ok = lambda.value.(*top); ok {
+ return true
+ }
+ }
+ return false
+ }
+ if o.left == nil {
+ return o.right.isDotDotDot()
+ }
+ if o.right == nil {
+ return o.left.isDotDotDot()
+ }
+ return o.left.isDotDotDot() && o.right.isDotDotDot()
+}
+
+// constraint returns the unification of all constraints for which arg matches
+// the key filter. doc contains the documentation of all applicable fields.
+func (o *optionals) constraint(ctx *context, label evaluated) (u value, doc *docNode) {
+ if o == nil {
+ return nil, nil
+ }
+ add := func(v value) {
+ if v != nil {
+ if u == nil {
+ u = v
+ } else {
+ u = mkBin(ctx, token.NoPos, opUnify, u, v)
+ }
+ }
+ }
+ v, doc1 := o.left.constraint(ctx, label)
+ add(v)
+ v, doc2 := o.right.constraint(ctx, label)
+ add(v)
+
+ if doc1 != nil || doc2 != nil {
+ doc = &docNode{left: doc1, right: doc2}
+ }
+
+ arg := label
+ if arg == nil {
+ arg = &basicType{k: stringKind}
+ }
+
+ for _, s := range o.fields {
+ if s.key != nil {
+ if label == nil {
+ continue
+ }
+ key := s.key.evalPartial(ctx)
+ if v := binOp(ctx, label, opUnify, key, label); isBottom(v) {
+ continue
+ }
+ }
+ fn, ok := ctx.manifest(s.value).(*lambdaExpr)
+ if !ok {
+ // create error
+ continue
+ }
+ add(fn.call(ctx, s.value, arg))
+ if f, _ := s.value.base().syntax().(*ast.Field); f != nil {
+ doc = &docNode{n: f, left: doc}
+ }
+ }
+ return u, doc
+}
+
+func (o *optionals) rewrite(fn func(value) value) (c *optionals, err evaluated) {
+ if o == nil {
+ return nil, nil
+ }
+
+ left, err := o.left.rewrite(fn)
+ if err != nil {
+ return nil, err
+ }
+ right, err := o.right.rewrite(fn)
+ if err != nil {
+ return nil, err
+ }
+
+ fields := make([]optionalSet, len(o.fields))
+ for i, s := range o.fields {
+ if s.key != nil {
+ s.key = fn(s.key)
+ if b, ok := s.key.(*bottom); ok {
+ return nil, b
+ }
+ }
+ s.value = fn(s.value)
+ if b, ok := s.value.(*bottom); ok {
+ return nil, b
+ }
+ fields[i] = s
+ }
+
+ return &optionals{o.closed, o.op, left, right, fields}, nil
+}
+
type closeMode byte
const (
@@ -677,16 +925,17 @@
return x.closeStatus.isClosed()
}
-func (x *structLit) addTemplate(ctx *context, pos token.Pos, t value) {
- if x.template == nil {
- x.template = t
- } else {
- x.template = mkBin(ctx, pos, opUnify, x.template, t)
+func (x *structLit) addTemplate(ctx *context, pos token.Pos, key, value value) {
+ if x.optionals == nil {
+ x.optionals = &optionals{}
}
+ x.optionals.add(ctx, key, value)
}
-func (x *structLit) allows(f label) bool {
- return !x.closeStatus.isClosed() || f&hidden != 0
+func (x *structLit) allows(ctx *context, f label) bool {
+ return !x.closeStatus.isClosed() ||
+ f&hidden != 0 ||
+ x.optionals.allows(ctx, f)
}
func newStruct(src source) *structLit {
@@ -702,8 +951,8 @@
func (x *structLit) Swap(i, j int) { x.arcs[i], x.arcs[j] = x.arcs[j], x.arcs[i] }
func (x *structLit) close() *structLit {
- if x.template != nil {
- return x // there is nothing to close as it is already fully defined.
+ if x.optionals.isFull() {
+ return x
}
newS := *x
@@ -779,7 +1028,7 @@
v := x.arcs[i].v.evalPartial(ctx)
ctx.evalStack = popped
- var doc *ast.Field
+ var doc *docNode
v, doc = x.applyTemplate(ctx, i, v)
// only place to apply template?
@@ -799,7 +1048,7 @@
updateCloseStatus(ctx, v)
x.arcs[i].cache = v
if doc != nil {
- x.arcs[i].docs = &docNode{n: doc, left: x.arcs[i].docs}
+ x.arcs[i].docs = &docNode{left: doc, right: x.arcs[i].docs}
}
if len(ctx.evalStack) == 0 {
if err := ctx.processDelayedConstraints(); err != nil {
@@ -889,21 +1138,23 @@
}
}
-func (x *structLit) applyTemplate(ctx *context, i int, v evaluated) (evaluated, *ast.Field) {
- if x.template != nil {
- fn, err := evalLambda(ctx, x.template, x.closeStatus != 0)
- if err != nil {
- return err, nil
- }
- name := ctx.labelStr(x.arcs[i].feature)
- arg := &stringLit{x.baseValue, name, nil}
- w := fn.call(ctx, x, arg).evalPartial(ctx)
- v = binOp(ctx, x, opUnify, v, w)
-
- f, _ := x.template.base().syntax().(*ast.Field)
- return v, f
+func (x *structLit) applyTemplate(ctx *context, i int, v evaluated) (e evaluated, doc *docNode) {
+ if x.optionals == nil {
+ return v, nil
}
- return v, nil
+
+ name := ctx.labelStr(x.arcs[i].feature)
+ arg := &stringLit{x.baseValue, name, nil}
+
+ val, doc := x.optionals.constraint(ctx, arg)
+ if val != nil {
+ v = binOp(ctx, x, opUnify, v, val.evalPartial(ctx))
+ }
+
+ if x.closeStatus != 0 {
+ updateCloseStatus(ctx, v)
+ }
+ return v, doc
}
// A label is a canonicalized feature name.
@@ -1006,6 +1257,7 @@
if err == nil {
if x.closeStatus.shouldClose() {
x.closeStatus = isClosed
+ y.optionals = y.optionals.close()
}
x.closeStatus |= shouldFinalize
y.closeStatus = x.closeStatus