cue: allow schema mode in subsumption
Change-Id: I33f37b22496235d07930a1e0ede7f085c7cae123
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5343
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/op.go b/cue/op.go
index 5706c2d..f465692 100644
--- a/cue/op.go
+++ b/cue/op.go
@@ -64,28 +64,29 @@
)
var opToOp = map[op]Op{
- opUnify: AndOp,
- opDisjunction: OrOp,
- opLand: BooleanAndOp,
- opLor: BooleanOrOp,
- opEql: EqualOp,
- opNot: NotOp,
- opNeq: NotEqualOp,
- opLss: LessThanOp,
- opLeq: LessThanEqualOp,
- opGtr: GreaterThanOp,
- opGeq: GreaterThanEqualOp,
- opMat: RegexMatchOp,
- opNMat: NotRegexMatchOp,
- opAdd: AddOp,
- opSub: SubtractOp,
- opMul: MultiplyOp,
- opQuo: FloatQuotientOp,
- opRem: FloatRemainOp,
- opIQuo: IntQuotientOp,
- opIRem: IntRemainderOp,
- opIDiv: IntDivideOp,
- opIMod: IntModuloOp,
+ opUnify: AndOp,
+ opUnifyUnchecked: AndOp,
+ opDisjunction: OrOp,
+ opLand: BooleanAndOp,
+ opLor: BooleanOrOp,
+ opEql: EqualOp,
+ opNot: NotOp,
+ opNeq: NotEqualOp,
+ opLss: LessThanOp,
+ opLeq: LessThanEqualOp,
+ opGtr: GreaterThanOp,
+ opGeq: GreaterThanEqualOp,
+ opMat: RegexMatchOp,
+ opNMat: NotRegexMatchOp,
+ opAdd: AddOp,
+ opSub: SubtractOp,
+ opMul: MultiplyOp,
+ opQuo: FloatQuotientOp,
+ opRem: FloatRemainOp,
+ opIQuo: IntQuotientOp,
+ opIRem: IntRemainderOp,
+ opIDiv: IntDivideOp,
+ opIMod: IntModuloOp,
}
var opToString = map[Op]string{
diff --git a/cue/subsume.go b/cue/subsume.go
index cc6200e..57834a5 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -18,6 +18,7 @@
"bytes"
"cuelang.org/go/cue/token"
+ "cuelang.org/go/internal"
)
// TODO: it probably makes sense to have only two modes left: subsuming a schema
@@ -45,7 +46,11 @@
} else {
b = ctx.mkErr(src, b, "%v", b)
}
- return w.toErr(b)
+ err := w.toErr(b)
+ if s.inexact {
+ err = internal.DecorateError(internal.ErrInexact, err)
+ }
+ return err
}
return nil
}
@@ -54,6 +59,8 @@
ctx *context
mode subsumeMode
+ inexact bool // If true, the result could be a false negative.
+
// recorded values where an error occurred.
gt, lt evaluated
missing label
@@ -71,8 +78,11 @@
// TODO: may be unnecessary now subFinal is available.
subNoOptional
- // the subsumed value is final
+ // The subsumed value is final.
subFinal
+
+ // subSchema is used to compare schema. It should ignore closedness.
+ subSchema
)
// TODO: improve upon this highly inefficient implementation. There should
@@ -153,6 +163,7 @@
if x.optionals != nil && !ignoreOptional {
if s.mode&subFinal == 0 {
// TODO: also cross-validate optional fields in the schema case.
+ s.inexact = true
return false
}
for _, b := range o.arcs {
@@ -168,6 +179,7 @@
}
}
if len(x.comprehensions) > 0 {
+ s.inexact = true
return false
}
if x.emit != nil {
@@ -176,6 +188,9 @@
}
}
+ xClosed := x.closeStatus.shouldClose() && s.mode&subSchema == 0
+ oClosed := o.closeStatus.shouldClose() && s.mode&subSchema == 0
+
// all arcs in n must exist in v and its values must subsume.
for _, a := range x.arcs {
if a.optional && ignoreOptional {
@@ -192,7 +207,7 @@
// thus subsumed. Technically, this is even true if a is not
// optional, but in that case it means that o is invalid, so
// return false regardless
- if a.optional && (o.closeStatus.shouldClose() || s.mode&subFinal != 0) {
+ if a.optional && (oClosed || s.mode&subFinal != 0) {
continue
}
// If field a is optional and has value top, neither the
@@ -212,8 +227,8 @@
}
}
// For closed structs, all arcs in b must exist in a.
- if x.closeStatus.shouldClose() {
- if !ignoreOptional && !o.closeStatus.shouldClose() && s.mode&subFinal == 0 {
+ if xClosed {
+ if !ignoreOptional && !oClosed && s.mode&subFinal == 0 {
return false
}
ignoreOptional = ignoreOptional || s.mode&subFinal != 0
@@ -422,6 +437,7 @@
continue outer
}
}
+ // TODO: should this be marked as inexact?
return false
}
return true
@@ -462,6 +478,7 @@
return true
}
}
+ // TODO: should this be marked as inexact?
return false
}
@@ -488,6 +505,7 @@
switch v := v.(type) {
case *stringLit:
// Be conservative if not ground.
+ s.inexact = true
return false
case *interpolation:
diff --git a/cue/subsume_test.go b/cue/subsume_test.go
index 96a1372..3d10d36 100644
--- a/cue/subsume_test.go
+++ b/cue/subsume_test.go
@@ -422,6 +422,17 @@
708: {subsumes: false, in: `a: {[string]: 1}, b: {foo: 2}`, mode: subFinal},
709: {subsumes: true, in: `a: {}, b: close({foo?: 1})`, mode: subFinal},
710: {subsumes: false, in: `a: {foo: [...string]}, b: {}`, mode: subFinal},
+
+ // Schema values
+ 800: {subsumes: true, in: `a: close({}), b: {foo: 1}`, mode: subSchema},
+ // TODO(eval): FIX
+ // 801: {subsumes: true, in: `a: {[string]: int}, b: {foo: 1}`, mode: subSchema},
+ 804: {subsumes: false, in: `a: {foo: 1}, b: {foo?: 1}`, mode: subSchema},
+ 805: {subsumes: true, in: `a: close({}), b: {foo?: 1}`, mode: subSchema},
+ 806: {subsumes: true, in: `a: close({}), b: close({foo?: 1})`, mode: subSchema},
+ 807: {subsumes: true, in: `a: {}, b: close({})`, mode: subSchema},
+ 808: {subsumes: false, in: `a: {[string]: 1}, b: {foo: 2}`, mode: subSchema},
+ 809: {subsumes: true, in: `a: {}, b: close({foo?: 1})`, mode: subSchema},
}
re := regexp.MustCompile(`a: (.*).*b: ([^\n]*)`)
diff --git a/cue/types.go b/cue/types.go
index 05c0fbb..0d7667f 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -663,6 +663,14 @@
return Value{v.idx, &valueData{v.path, i, a}}
}
+func (v Value) makeElem(x value) Value {
+ return Value{v.idx, &valueData{v.path, 0, arc{
+ optional: true,
+ v: x,
+ cache: evalValue(v.ctx(), x),
+ }}}
+}
+
func (v Value) eval(ctx *context) evaluated {
if v.path == nil || v.path.cache == nil {
panic("undefined value")
@@ -670,13 +678,25 @@
return ctx.manifest(v.path.cache)
}
+func evalValue(ctx *context, v value) evaluated {
+ x := v.evalPartial(ctx)
+ if st, ok := x.(*structLit); ok {
+ var err *bottom
+ x, err = st.expandFields(ctx)
+ if err != nil {
+ x = err
+ }
+ }
+ return x
+}
+
// Eval resolves the references of a value and returns the result.
// This method is not necessary to obtain concrete values.
func (v Value) Eval() Value {
if v.path == nil {
return v
}
- return remakeValue(v, v.path.v.evalPartial(v.ctx()))
+ return remakeValue(v, evalValue(v.ctx(), v.path.v))
}
// Default reports the default value and whether it existed. It returns the
@@ -687,7 +707,7 @@
}
u := v.path.cache
if u == nil {
- u = v.path.v.evalPartial(v.ctx())
+ u = evalValue(v.ctx(), v.path.v)
}
x := v.ctx().manifest(u)
if x != u {
@@ -1040,9 +1060,9 @@
if t == nil {
break
}
- return newValueRoot(ctx, t), true
+ return v.makeElem(t), true
case *list:
- return newValueRoot(ctx, x.typ), true
+ return v.makeElem(x.typ), true
}
return Value{}, false
}
@@ -1453,6 +1473,9 @@
if o.final {
mode |= subFinal | subChoose
}
+ if o.ignoreClosedness {
+ mode |= subSchema
+ }
return subsumes(v, w, mode)
}
@@ -1685,6 +1708,7 @@
omitAttrs bool
resolveReferences bool
final bool
+ ignoreClosedness bool // used for comparing APIs
docs bool
disallowCycles bool // implied by concrete
}
@@ -1705,6 +1729,13 @@
}
}
+// Schema specifies the input is a Schema. Used by Subsume.
+func Schema() Option {
+ return func(o *options) {
+ o.ignoreClosedness = true
+ }
+}
+
// Concrete ensures that all values are concrete.
//
// For Validate this means it returns an error if this is not the case.
diff --git a/internal/internal.go b/internal/internal.go
index b4ecd61..cdf28dc 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -27,6 +27,7 @@
"strings"
"github.com/cockroachdb/apd/v2"
+ "golang.org/x/xerrors"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
@@ -187,3 +188,21 @@
}
return filepath.Join(root, "cue.mod", "gen")
}
+
+var ErrInexact = errors.New("inexact subsumption")
+
+func DecorateError(info error, err errors.Error) errors.Error {
+ return &decorated{cueError: err, info: info}
+}
+
+type cueError = errors.Error
+
+type decorated struct {
+ cueError
+
+ info error
+}
+
+func (e *decorated) Is(err error) bool {
+ return xerrors.Is(e.info, err) || xerrors.Is(e.cueError, err)
+}