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)
+}