pkg/encoding/yaml: validate using instance check

Assume that the YAML value is final. This seems the
expected behavior in most cases.

ValidatePartial now has the original meaning.

Fixes #266

Change-Id: I7285bc5327ce661c720c9cd2254dc9fe8f9bcdc3
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4821
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/vet_yaml.txt b/cmd/cue/cmd/testdata/script/vet_yaml.txt
new file mode 100644
index 0000000..fe8c513
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/vet_yaml.txt
@@ -0,0 +1,43 @@
+! cue vet ./yaml.cue
+cmp stderr expect-stderr
+
+-- expect-stderr --
+phrases: error in call to encoding/yaml.Validate: missing field "text":
+    ./yaml.cue:19:10
+    ./yaml.cue:4:12
+    ./yaml.cue:11:17
+    yaml.Validate:4:6
+-- yaml.cue --
+import "encoding/yaml"
+
+// Phrases defines a schema for a valid phrase.
+Phrases :: {
+	phrases: {
+		[string]: Phrase
+	}
+
+	Phrase :: {
+		lang:         LanguageTag
+		text:         !=""
+		attribution?: !="" // must be non-empty when specified
+	}
+	LanguageTag :: =~"^[a-zA-Z0-9-_]{2,}$" | false
+}
+
+// phrases is a YAML string with a field phrases that is a map of Phrase
+// objects.
+phrases: yaml.Validate(Phrases)
+
+phrases: """
+    phrases:
+      # A quote from Mark Twain.
+      quote1:
+        lang: en
+        attribution: Mark Twain
+
+      # A Norwegian proverb.
+      proverb:
+        lang: no
+        text: Stemmen som sier at du ikke klarer det, lyver.
+    """
+
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index d1ebbd0..9e427e4 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -120,6 +120,18 @@
 		test("encoding/yaml", `yaml.Validate("a: 2\n---\na: 4", {a:<5})`),
 		`true`,
 	}, {
+		test("encoding/yaml", `yaml.Validate("a: 2\n", {a:<5, b:int})`),
+		`_|_(error in call to encoding/yaml.Validate: value not an instance)`,
+	}, {
+		test("encoding/yaml", `yaml.ValidatePartial("a: 2\n---\na: 4", {a:<3})`),
+		`_|_(error in call to encoding/yaml.ValidatePartial: invalid value 4 (out of bound <3))`,
+	}, {
+		test("encoding/yaml", `yaml.ValidatePartial("a: 2\n---\na: 4", {a:<5})`),
+		`true`,
+	}, {
+		test("encoding/yaml", `yaml.ValidatePartial("a: 2\n", {a:<5, b:int})`),
+		`true`,
+	}, {
 		test("strconv", `strconv.FormatUint(64, 16)`),
 		`"40"`,
 	}, {
diff --git a/cue/builtins.go b/cue/builtins.go
index e8f631b..d94d39f 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -30,14 +30,15 @@
 	"unicode"
 	"unicode/utf8"
 
+	"github.com/cockroachdb/apd/v2"
+	goyaml "github.com/ghodss/yaml"
+	"golang.org/x/net/idna"
+
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/literal"
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/internal"
 	"cuelang.org/go/internal/third_party/yaml"
-	"github.com/cockroachdb/apd/v2"
-	goyaml "github.com/ghodss/yaml"
-	"golang.org/x/net/idna"
 )
 
 func init() {
@@ -723,6 +724,40 @@
 								return false, err
 							}
 
+							if err := v.Subsume(inst.Value(), Final()); err != nil {
+								return false, err
+							}
+						}
+					}()
+				}
+			},
+		}, {
+			Name:   "ValidatePartial",
+			Params: []kind{bytesKind | stringKind, topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				b, v := c.bytes(0), c.value(1)
+				if c.do() {
+					c.ret, c.err = func() (interface{}, error) {
+						d, err := yaml.NewDecoder("yaml.ValidatePartial", b)
+						if err != nil {
+							return false, err
+						}
+						r := internal.GetRuntime(v).(*Runtime)
+						for {
+							expr, err := d.Decode()
+							if err != nil {
+								if err == io.EOF {
+									return true, nil
+								}
+								return false, err
+							}
+
+							inst, err := r.CompileExpr(expr)
+							if err != nil {
+								return false, err
+							}
+
 							if x := v.Unify(inst.Value()); x.Err() != nil {
 								return false, x.Err()
 							}
@@ -3591,19 +3626,20 @@
 		native: []*builtin{{}},
 		cue: `{
 	Command: {
-		tasks: {
-			[name=string]: Task
-		}
-		$type:   "tool.Command"
-		$name:   !=""
-		$usage?: =~"^\($name) "
+		$usage?: string
 		$short?: string
 		$long?:  string
+		Tasks
+	}
+	Tasks: Task | {
+		[name=string]: Tasks
 	}
 	Task: {
-		$type: "tool.Task"
-		$id:   =~"\\."
+		$type:   "tool.Task"
+		$id:     =~"\\."
+		$after?: Task | [...Task]
 	}
+	Name :: =~"^\\PL([-](\\PL|\\PN))*$"
 }`,
 	},
 	"tool/cli": &builtinPkg{
@@ -3619,11 +3655,10 @@
 		native: []*builtin{{}},
 		cue: `{
 	Run: {
-		$id:      *"tool/exec.Run" | "exec"
-		cmd:      string | [string, ...string]
-		install?: string | [string, ...string]
+		$id: *"tool/exec.Run" | "exec"
+		cmd: string | [string, ...string]
 		env: {
-			[string]: string
+			[string]: string | [...=~"="]
 		}
 		stdout:  *null | string | bytes
 		stderr:  *null | string | bytes
@@ -3704,8 +3739,8 @@
 	"tool/os": &builtinPkg{
 		native: []*builtin{{}},
 		cue: `{
-	Value :: bool | number | *string | null
 	Name ::  !="" & !~"^[$]"
+	Value :: bool | number | *string | null
 	Setenv: {
 		[Name]: Value
 		$id:    "tool/os.Setenv"
diff --git a/cue/subsume.go b/cue/subsume.go
index b4bc2b7..cc6200e 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -30,17 +30,21 @@
 	s := subsumer{ctx: ctx, mode: mode}
 	if !s.subsumes(gt, lt) {
 		var b *bottom
-		var ok bool
 		src := binSrc(token.NoPos, opUnify, gt, lt)
-		src2 := src
 		if s.gt != nil && s.lt != nil {
 			src := binSrc(token.NoPos, opUnify, s.gt, s.lt)
-			b, ok = binOp(ctx, src, opUnify, s.gt, s.lt).(*bottom)
+			var ok bool
+			if s.missing != 0 {
+				b = ctx.mkErr(src, "missing field %q", ctx.labelStr(s.missing))
+			} else if b, ok = binOp(ctx, src, opUnify, s.gt, s.lt).(*bottom); !ok {
+				b = ctx.mkErr(src, "value not an instance")
+			}
 		}
-		if !ok {
+		if b == nil {
 			b = ctx.mkErr(src, "value not an instance")
+		} else {
+			b = ctx.mkErr(src, b, "%v", b)
 		}
-		b = ctx.mkErr(src2, b, "%v", b)
 		return w.toErr(b)
 	}
 	return nil
@@ -51,7 +55,8 @@
 	mode subsumeMode
 
 	// recorded values where an error occurred.
-	gt, lt evaluated
+	gt, lt  evaluated
+	missing label
 }
 
 type subsumeMode int
@@ -196,7 +201,9 @@
 				if a.optional && isTop(a.v) {
 					continue
 				}
+				s.missing = a.feature
 				s.gt = a.val()
+				s.lt = o
 				return false
 			} else if a.definition != b.definition {
 				return false
diff --git a/pkg/encoding/yaml/manual.go b/pkg/encoding/yaml/manual.go
index 286f754..053ab6b 100644
--- a/pkg/encoding/yaml/manual.go
+++ b/pkg/encoding/yaml/manual.go
@@ -58,7 +58,7 @@
 	return yaml.Unmarshal("", data)
 }
 
-// Validate validates YAML and confirms it matches the constraints
+// Validate validates YAML and confirms it is an instance of the schema
 // specified by v. If the YAML source is a stream, every object must match v.
 func Validate(b []byte, v cue.Value) (bool, error) {
 	d, err := yaml.NewDecoder("yaml.Validate", b)
@@ -80,6 +80,36 @@
 			return false, err
 		}
 
+		if err := v.Subsume(inst.Value(), cue.Final()); err != nil {
+			return false, err
+		}
+	}
+}
+
+// ValidatePartial validates YAML and confirms it matches the constraints
+// specified by v using unification. This means that b must be consistent with,
+// but does not have to be an instance of v. If the YAML source is a stream,
+// every object must match v.
+func ValidatePartial(b []byte, v cue.Value) (bool, error) {
+	d, err := yaml.NewDecoder("yaml.ValidatePartial", b)
+	if err != nil {
+		return false, err
+	}
+	r := internal.GetRuntime(v).(*cue.Runtime)
+	for {
+		expr, err := d.Decode()
+		if err != nil {
+			if err == io.EOF {
+				return true, nil
+			}
+			return false, err
+		}
+
+		inst, err := r.CompileExpr(expr)
+		if err != nil {
+			return false, err
+		}
+
 		if x := v.Unify(inst.Value()); x.Err() != nil {
 			return false, x.Err()
 		}
diff --git a/pkg/tool/exec/exec_test.go b/pkg/tool/exec/exec_test.go
index fb50513..69e9484 100644
--- a/pkg/tool/exec/exec_test.go
+++ b/pkg/tool/exec/exec_test.go
@@ -39,13 +39,13 @@
 			WHEN: "Now!"
 		}
 		`,
-		env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!" },
+		env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!"},
 	}, {
 		val: `
 		cmd: "echo"
 		env: [ "WHO=World", "WHAT=Hello", "WHEN=Now!" ]
 		`,
-		env: []string{ "WHO=World", "WHAT=Hello", "WHEN=Now!" },
+		env: []string{"WHO=World", "WHAT=Hello", "WHEN=Now!"},
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {