cue: ignore optional fields for Equals

Equal is only defined for for concrete values, so it
makes sense to ignore optional fields.

Issue #200
Issue #207

Change-Id: I96e9fc36a986cc9e95a412cdab4409a600eeab84
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4300
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/subsume.go b/cue/subsume.go
index d9a2257..5cc134f 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -24,8 +24,18 @@
 	// subChoose ensures values are elected before doing a subsumption. This
 	// feature is on the conservative side and may result in false negatives.
 	subChoose subsumeMode = 1 << iota
+
+	// subNoOptional ignores optional fields for the purpose of subsumption.
+	// This option is predominantly intended for implementing equality checks.
+	subNoOptional
 )
 
+// TODO: improve upon this highly inefficient implementation. There should
+// be a dedicated equal function once the dust settles.
+func equals(c *context, x, y value) bool {
+	return subsumes(c, x, y, subNoOptional) && subsumes(c, y, x, subNoOptional)
+}
+
 // subsumes checks gt subsumes lt. If any of the values contains references or
 // unevaluated expressions, structural subsumption is performed. This means
 // subsumption is conservative; it may return false when a guarantee for
@@ -84,10 +94,14 @@
 }
 
 func (x *structLit) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
+	ignoreOptional := mode&subNoOptional != 0
 	if o, ok := v.(*structLit); ok {
 		// TODO: consider what to do with templates. Perhaps we should always
 		// do subsumption on fully evaluated structs.
-		if len(x.comprehensions) > 0 || x.optionals != nil {
+		if x.optionals != nil && !ignoreOptional {
+			return false
+		}
+		if len(x.comprehensions) > 0 {
 			return false
 		}
 		if x.emit != nil {
@@ -98,6 +112,9 @@
 
 		// all arcs in n must exist in v and its values must subsume.
 		for _, a := range x.arcs {
+			if a.optional && ignoreOptional {
+				continue
+			}
 			b := o.lookup(ctx, a.feature)
 			if !a.optional && b.optional {
 				return false
@@ -114,10 +131,13 @@
 		}
 		// For closed structs, all arcs in b must exist in a.
 		if x.closeStatus.shouldClose() {
-			if !o.closeStatus.shouldClose() {
+			if !ignoreOptional && !o.closeStatus.shouldClose() {
 				return false
 			}
 			for _, b := range o.arcs {
+				if ignoreOptional && b.optional {
+					continue
+				}
 				a := x.lookup(ctx, b.feature)
 				if a.val() == nil {
 					return false
diff --git a/cue/subsume_test.go b/cue/subsume_test.go
index decf340..0a95aa0 100644
--- a/cue/subsume_test.go
+++ b/cue/subsume_test.go
@@ -372,6 +372,16 @@
 		// TODO: handle optionals.
 		431: {subsumes: false, in: `a: {[_]: int}, b: {[_]: 2}`},
 
+		440: {subsumes: true, in: `a: {foo?: 1}, b: {}`, mode: subNoOptional},
+		441: {subsumes: true, in: `a: {}, b: {foo?: 1}`, mode: subNoOptional},
+		442: {subsumes: true, in: `a: {foo?: 1}, b: {foo: 1}`, mode: subNoOptional},
+		443: {subsumes: true, in: `a: {foo?: 1}, b: {foo?: 1}`, mode: subNoOptional},
+		444: {subsumes: false, in: `a: {foo: 1}, b: {foo?: 1}`, mode: subNoOptional},
+		445: {subsumes: true, in: `a: close({}), b: {foo?: 1}`, mode: subNoOptional},
+		446: {subsumes: true, in: `a: close({}), b: close({foo?: 1})`, mode: subNoOptional},
+		447: {subsumes: true, in: `a: {}, b: close({})`, mode: subNoOptional},
+		448: {subsumes: true, in: `a: {}, b: close({foo?: 1})`, mode: subNoOptional},
+
 		// Lists
 		506: {subsumes: true, in: `a: [], b: [] `},
 		507: {subsumes: true, in: `a: [1], b: [1] `},
@@ -392,8 +402,10 @@
 		605: {subsumes: true, in: `a: {a: 1}, b: close({a: 1, b: 1})`},
 		606: {subsumes: true, in: `a: close({b?: 1}), b: close({b: 1})`},
 		607: {subsumes: false, in: `a: close({b: 1}), b: close({b?: 1})`},
+		608: {subsumes: true, in: `a: {}, b: close({})`},
+		609: {subsumes: true, in: `a: {}, b: close({foo?: 1})`},
 
-		// Definitions are not values.
+		// Definitions are not regular fields.
 		610: {subsumes: false, in: `a: {a :: 1}, b: {a: 1}`},
 		611: {subsumes: false, in: `a: {a: 1}, b: {a :: 1}`},
 	}
diff --git a/cue/types.go b/cue/types.go
index 754af56..273fd24 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -1363,7 +1363,7 @@
 	return u
 }
 
-// Equals reports whether two values are equal.
+// Equals reports whether two values are equal, ignoring optional fields.
 // The result is undefined for incomplete values.
 func (v Value) Equals(other Value) bool {
 	if v.path == nil || other.path == nil {
@@ -1371,8 +1371,7 @@
 	}
 	x := v.path.val()
 	y := other.path.val()
-	// TODO: improve upon this highly inefficient implementation.
-	return subsumes(v.ctx(), x, y, 0) && subsumes(v.ctx(), y, x, 0)
+	return equals(v.ctx(), x, y)
 }
 
 // Format prints a debug version of a value.