encoding/yaml: fix bug where an empty document is not treated as null

Change-Id: Ibb763b5e44d6c80d569db8df863d79631d0d2935
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6561
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/yaml/yaml.go b/encoding/yaml/yaml.go
index 80c19b9..0787077 100644
--- a/encoding/yaml/yaml.go
+++ b/encoding/yaml/yaml.go
@@ -37,11 +37,14 @@
 	}
 	for {
 		expr, err := d.Decode()
-		if err == io.EOF {
-			break
-		}
 		if err != nil {
-			return nil, err
+			if err != io.EOF {
+				return nil, err
+			}
+			if expr != nil {
+				a = append(a, expr)
+			}
+			break
 		}
 		a = append(a, expr)
 	}
diff --git a/encoding/yaml/yaml_test.go b/encoding/yaml/yaml_test.go
index a9ce43d..304b488 100644
--- a/encoding/yaml/yaml_test.go
+++ b/encoding/yaml/yaml_test.go
@@ -27,9 +27,19 @@
 	testCases := []struct {
 		name     string
 		yaml     string
+		yamlOut  string
 		want     string
 		isStream bool
 	}{{
+		name:    "empty",
+		yaml:    "",
+		yamlOut: "null",
+		want:    "null",
+	}, {
+		name:     "empty stream",
+		want:     "null",
+		isStream: true,
+	}, {
 		name: "string literal",
 		yaml: `foo`,
 		want: `"foo"`,
@@ -54,7 +64,37 @@
 }]`,
 		isStream: true,
 	}, {
-		name:     "emtpy",
+		name: "stream with null",
+		yaml: `
+---
+a: foo
+---
+---
+b: bar
+c: baz
+---
+`,
+		// Not sure if a leading document separator should be gobbled, but the
+		// YAML parser seems to think so. This could have something to do with
+		// the fact that the document separator is really an "end of directives"
+		// marker, while ... means "end of document". YAML is hard!
+		yamlOut: `a: foo
+---
+null
+---
+b: bar
+c: baz
+---
+null
+`,
+		// TODO(bug): seems like bug in yaml parser. Try moving to yaml.v3,
+		// or validate that this is indeed a correct interpretation.
+		want: `[{
+	a: "foo"
+}, null, {
+	b: "bar"
+	c: "baz"
+}, null]`,
 		isStream: true,
 	}}
 	r := &cue.Runtime{}
@@ -66,7 +106,7 @@
 			}
 			b, _ := format.Node(f)
 			if got := strings.TrimSpace(string(b)); got != tc.want {
-				t.Errorf("got %q; want %q", got, tc.want)
+				t.Errorf("Extract:\ngot  %q\nwant %q", got, tc.want)
 			}
 
 			inst, err := Decode(r, tc.name, tc.yaml)
@@ -79,7 +119,12 @@
 			}
 			b, _ = format.Node(n)
 			if got := strings.TrimSpace(string(b)); got != tc.want {
-				t.Errorf("got %q; want %q", got, tc.want)
+				t.Errorf("Decode:\ngot  %q\nwant %q", got, tc.want)
+			}
+
+			yamlOut := tc.yaml
+			if tc.yamlOut != "" {
+				yamlOut = tc.yamlOut
 			}
 
 			inst, _ = r.Compile(tc.name, tc.want)
@@ -88,8 +133,8 @@
 				if err != nil {
 					t.Error(err)
 				}
-				if got := strings.TrimSpace(string(b)); got != tc.yaml {
-					t.Errorf("got %q; want %q", got, tc.yaml)
+				if got := strings.TrimSpace(string(b)); got != yamlOut {
+					t.Errorf("Encode:\ngot  %q\nwant %q", got, yamlOut)
 				}
 			} else {
 				iter, _ := inst.Value().List()
@@ -97,8 +142,8 @@
 				if err != nil {
 					t.Error(err)
 				}
-				if got := string(b); got != tc.yaml {
-					t.Errorf("got %q; want %q", got, tc.yaml)
+				if got := string(b); got != yamlOut {
+					t.Errorf("EncodeStream:\ngot  %q\nwant %q", got, yamlOut)
 				}
 			}
 		})
diff --git a/internal/third_party/yaml/yaml.go b/internal/third_party/yaml/yaml.go
index 20ef3a1..5177614 100644
--- a/internal/third_party/yaml/yaml.go
+++ b/internal/third_party/yaml/yaml.go
@@ -86,8 +86,9 @@
 
 // A Decorder reads and decodes YAML values from an input stream.
 type Decoder struct {
-	strict bool
-	parser *parser
+	strict    bool
+	firstDone bool
+	parser    *parser
 }
 
 // NewDecoder returns a new decoder that reads from r.
@@ -113,8 +114,12 @@
 	defer handleErr(&err)
 	node := dec.parser.parse()
 	if node == nil {
-		return nil, io.EOF
+		if !dec.firstDone {
+			expr = ast.NewNull()
+		}
+		return expr, io.EOF
 	}
+	dec.firstDone = true
 	expr = d.unmarshal(node)
 	if len(d.terrors) > 0 {
 		return nil, &TypeError{d.terrors}