cue: implement closed structs for subsumption

Also make it clearer in the API when an operation
applies to only values and when to all fields,
including definitions.

Issue #40

Change-Id: Ibfbd5fa4262a234b3c93afdb2b02214cc8b36e5c
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2945
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin.go b/cue/builtin.go
index f7dbefa..58d0bcf 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -120,7 +120,7 @@
 		v := c.value(0)
 		switch v.Kind() {
 		case StructKind:
-			s, _ := v.structVal(c.ctx)
+			s, _ := v.structValData(c.ctx)
 			c.ret = s.Len()
 		case ListKind:
 			i := 0
diff --git a/cue/examples_test.go b/cue/examples_test.go
index d065e87..7505f3b 100644
--- a/cue/examples_test.go
+++ b/cue/examples_test.go
@@ -16,6 +16,7 @@
 
 import (
 	"fmt"
+	"log"
 
 	"cuelang.org/go/cue"
 )
@@ -64,3 +65,53 @@
 	// {2 4}
 	// json: cannot unmarshal string into Go struct field ab.B of type int
 }
+
+func ExampleSubsumes() {
+	// Check compatibility of successive APIs.
+	var r cue.Runtime
+
+	inst, err := r.Compile("apis", `
+	// Release notes:
+	// - You can now specify your age and your hobby!
+	V1 :: {
+		age:   >=0 & <=100
+		hobby: string
+	}
+
+	// Release notes:
+	// - People get to be older than 100, so we relaxed it.
+	// - It seems not many people have a hobby, so we made it optional.
+	V2 :: {
+		age:    >=0 & <=150 // people get older now
+		hobby?: string      // some people don't have a hobby
+	}
+
+	// Release notes:
+	// - Actually no one seems to have a hobby nowadays anymore,
+	//   so we dropped the field.
+	V3 :: {
+		age: >=0 & <=150
+	}`)
+
+	if err != nil {
+		fmt.Println(err)
+		// handle error
+	}
+	v1, err1 := inst.LookupField("V1")
+	v2, err2 := inst.LookupField("V2")
+	v3, err3 := inst.LookupField("V3")
+	if err1 != nil || err2 != nil || err3 != nil {
+		log.Println(err1, err2, err3)
+	}
+
+	// Is V2 backwards compatible with V1? In other words, does V2 subsume V1?
+	pass := v2.Value.Subsumes(v1.Value)
+	fmt.Println("V2 is backwards compatible with V1:", pass)
+
+	pass = v3.Value.Subsumes(v2.Value)
+	fmt.Println("V3 is backwards compatible with V2:", pass)
+
+	// Output:
+	// V2 is backwards compatible with V1: true
+	// V3 is backwards compatible with V2: false
+}
diff --git a/cue/instance.go b/cue/instance.go
index d79a0cd..8493ae0 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -15,6 +15,8 @@
 package cue
 
 import (
+	goast "go/ast"
+
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/errors"
@@ -231,7 +233,7 @@
 
 	ctx := inst.newContext()
 	val := newValueRoot(ctx, inst.rootValue)
-	v, err := val.structVal(ctx)
+	v, err := val.structValFull(ctx)
 	if err != nil {
 		i.setError(val.toErr(err))
 		return i
@@ -252,16 +254,17 @@
 	return i
 }
 
-// Lookup reports the value starting from the top level struct (not the emitted
-// value), or an error if the path is not found.
-// The empty path returns the top-level configuration struct, regardless of
-// whether an emit value was specified.
+// Lookup reports the value at a path starting from the top level struct (not
+// the emitted value). The Exists method of the returned value will report false
+// if the path did not exist. The Err method reports if any error occurred
+// during evaluation. The empty path returns the top-level configuration struct,
+// regardless of whether an emit value was specified.
 func (inst *Instance) Lookup(path ...string) Value {
 	idx := inst.index
 	ctx := idx.newContext()
 	v := newValueRoot(ctx, inst.rootValue)
 	for _, k := range path {
-		obj, err := v.structVal(ctx)
+		obj, err := v.structValData(ctx)
 		if err != nil {
 			return Value{idx, &valueData{arc: arc{cache: err, v: err}}}
 		}
@@ -270,6 +273,31 @@
 	return v
 }
 
+// LookupField reports a Field at a path starting from v, or an error if the
+// path is not. The empty path returns v itself.
+//
+// It cannot look up hidden or unexported fields.
+func (inst *Instance) LookupField(path ...string) (f FieldInfo, err error) {
+	idx := inst.index
+	ctx := idx.newContext()
+	v := newValueRoot(ctx, inst.rootValue)
+	for i, k := range path {
+		s, err := v.Struct()
+		if err != nil {
+			return f, err
+		}
+
+		f, err = s.FieldByName(k)
+		if err != nil {
+			return f, err
+		}
+		if f.IsHidden || (i == 0 || f.IsDefinition) && !goast.IsExported(f.Name) {
+			return f, errNotFound
+		}
+	}
+	return f, err
+}
+
 // Fill creates a new instance with the values of the old instance unified with
 // the given value. It is not possible to update the emit value.
 func (inst *Instance) Fill(x interface{}, path ...string) (*Instance, error) {
diff --git a/cue/subsume.go b/cue/subsume.go
index 7b728af..0440e54 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -96,6 +96,8 @@
 			b := o.lookup(ctx, a.feature)
 			if !a.optional && b.optional {
 				return false
+			} else if a.definition != b.definition {
+				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
@@ -105,6 +107,15 @@
 				return false
 			}
 		}
+		// For closed structs, all arcs in b must exist in a.
+		if x.isClosed {
+			for _, b := range o.arcs {
+				a := x.lookup(ctx, b.feature)
+				if a.val() == nil {
+					return false
+				}
+			}
+		}
 	}
 	return !isBottom(v)
 }
diff --git a/cue/subsume_test.go b/cue/subsume_test.go
index 0a7d79a..97dd9eb 100644
--- a/cue/subsume_test.go
+++ b/cue/subsume_test.go
@@ -320,6 +320,9 @@
 		312: {subsumes: false, in: `a: !=2 & !=4, b: >3`},
 		313: {subsumes: true, in: `a: !=2 & !=4, b: >5`},
 
+		314: {subsumes: false, in: `a: >=0 & <=100, b: >=0 & <=150`},
+		315: {subsumes: true, in: `a: >=0 & <=150, b: >=0 & <=100`},
+
 		// Disjunctions
 		330: {subsumes: true, in: `a: >5, b: >10 | 8`},
 		331: {subsumes: false, in: `a: >8, b: >10 | 8`},
@@ -371,6 +374,18 @@
 		512: {subsumes: false, in: `a: [{b: "foo"}], b: [{b: string}] `},
 		513: {subsumes: false, in: `a: [{b: string}], b: [{b: "foo"}, ...{b: "foo"}] `},
 		520: {subsumes: false, in: `a: [_, int, ...], b: [int, string, ...string] `},
+
+		// Closed structs.
+		600: {subsumes: false, in: `a: close({}), b: {a: 1}`},
+		601: {subsumes: true, in: `a: close({a: 1}), b: {a: 1}`},
+		602: {subsumes: false, in: `a: close({a: 1, b: 1}), b: {a: 1}`},
+		603: {subsumes: false, in: `a: {a: 1}, b: close({})`},
+		604: {subsumes: true, in: `a: {a: 1}, b: close({a: 1})`},
+		605: {subsumes: true, in: `a: {a: 1}, b: close({a: 1, b: 1})`},
+
+		// Definitions are not values.
+		610: {subsumes: false, in: `a: {a :: 1}, b: {a: 1}`},
+		611: {subsumes: false, in: `a: {a: 1}, b: {a :: 1}`},
 	}
 
 	re := regexp.MustCompile(`a: (.*).*b: ([^\n]*)`)
@@ -395,7 +410,8 @@
 					b = arc.v
 				}
 			}
-			if got := subsumes(ctx, a, b, tc.mode); got != tc.subsumes {
+			got := subsumes(ctx, a, b, tc.mode)
+			if got != tc.subsumes {
 				t.Errorf("got %v; want %v (%v vs %v)", got, tc.subsumes, a.kind(), b.kind())
 			}
 		})
diff --git a/cue/types.go b/cue/types.go
index c31a2d2..6ca8760 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -18,6 +18,7 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	goast "go/ast"
 	"io"
 	"math"
 	"math/big"
@@ -716,7 +717,7 @@
 		i := Iterator{ctx: ctx, val: v, iter: l, len: len(l.elem.arcs)}
 		return marshalList(&i)
 	case structKind:
-		obj, _ := v.structVal(ctx)
+		obj, _ := v.structValData(ctx)
 		return obj.marshalJSON()
 	case bottomKind:
 		return nil, toMarshalErr(v, x.(*bottom))
@@ -1033,13 +1034,17 @@
 // a structVal.
 
 // structVal returns an structVal or an error if v is not a struct.
-func (v Value) structVal(ctx *context) (structValue, *bottom) {
+func (v Value) structValData(ctx *context) (structValue, *bottom) {
 	return v.structValOpts(ctx, options{
 		omitHidden:   true,
 		omitOptional: true,
 	})
 }
 
+func (v Value) structValFull(ctx *context) (structValue, *bottom) {
+	return v.structValOpts(ctx, options{})
+}
+
 // structVal returns an structVal or an error if v is not a struct.
 func (v Value) structValOpts(ctx *context, o options) (structValue, *bottom) {
 	v, _ = v.Default() // TODO: remove?
@@ -1131,8 +1136,9 @@
 	Name  string
 	Value Value
 
-	IsOptional bool
-	IsHidden   bool
+	IsDefinition bool
+	IsOptional   bool
+	IsHidden     bool
 }
 
 // field reports information about the ith field, i < o.Len().
@@ -1143,17 +1149,17 @@
 
 	v := Value{ctx.index, &valueData{s.v.path, uint32(i), a}}
 	str := ctx.labelStr(a.feature)
-	return FieldInfo{str, v, a.optional, a.feature&hidden != 0}
+	return FieldInfo{str, v, a.definition, a.optional, a.feature&hidden != 0}
 }
 
-func (s *Struct) FieldByName(name string) (FieldInfo, bool) {
+func (s *Struct) FieldByName(name string) (FieldInfo, error) {
 	f := s.v.ctx().strLabel(name)
 	for i, a := range s.s.arcs {
 		if a.feature == f {
-			return s.field(i), true
+			return s.field(i), nil
 		}
 	}
-	return FieldInfo{}, false
+	return FieldInfo{}, errNotFound
 }
 
 // Fields creates an iterator over the Struct's fields.
@@ -1175,14 +1181,15 @@
 	return Iterator{ctx: ctx, val: v, iter: obj.n, len: len(obj.n.arcs)}, nil
 }
 
-// Lookup reports the value starting from v, or an error if the path is not
-// found. The empty path returns v itself.
+// Lookup reports the value at a path starting from v.
+// The empty path returns v itself.
 //
-// Lookup cannot be used to look up hidden fields.
+// The Exists() method can be used to verify if the returned value existed.
+// Lookup cannot be used to look up hidden or optional fields or definitions.
 func (v Value) Lookup(path ...string) Value {
 	ctx := v.ctx()
 	for _, k := range path {
-		obj, err := v.structVal(ctx)
+		obj, err := v.structValData(ctx)
 		if err != nil {
 			// TODO: return a Value at the same location and a new error?
 			return newValueRoot(ctx, err)
@@ -1192,6 +1199,25 @@
 	return v
 }
 
+var errNotFound = errors.Newf(token.NoPos, "field not found")
+
+// LookupField reports information about a field of v.
+func (v Value) LookupField(path string) (FieldInfo, error) {
+	s, err := v.Struct()
+	if err != nil {
+		// TODO: return a Value at the same location and a new error?
+		return FieldInfo{}, err
+	}
+	f, err := s.FieldByName(path)
+	if err != nil {
+		return f, err
+	}
+	if f.IsHidden || f.IsDefinition && !goast.IsExported(path) {
+		return f, errNotFound
+	}
+	return f, err
+}
+
 // Template returns a function that represents the template definition for a
 // struct in a configuration file. It returns nil if v is not a struct kind or
 // if there is no template associated with the struct.
@@ -1502,7 +1528,8 @@
 }
 
 // Walk descends into all values of v, calling f. If f returns false, Walk
-// will not descent further.
+// will not descent further. It only visits values that are part of the data
+// model, so this excludes optional fields, hidden fields, and definitions.
 func (v Value) Walk(before func(Value) bool, after func(Value)) {
 	ctx := v.ctx()
 	switch v.Kind() {
@@ -1510,7 +1537,7 @@
 		if before != nil && !before(v) {
 			return
 		}
-		obj, _ := v.structVal(ctx)
+		obj, _ := v.structValData(ctx)
 		for i := 0; i < obj.Len(); i++ {
 			_, v := obj.At(i)
 			v.Walk(before, after)
diff --git a/cue/types_test.go b/cue/types_test.go
index ea8ef72..764228a 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -1992,7 +1992,7 @@
 			ctx, st := compileFile(t, tc.config)
 			v := newValueRoot(ctx, st)
 			for _, k := range strings.Split(tc.in, ".") {
-				obj, err := v.structVal(ctx)
+				obj, err := v.structValFull(ctx)
 				if err != nil {
 					t.Fatal(err)
 				}