cue: implement attribute declarations

This only allows attributes in places that can
clearly be considered to be part of the
package fields. The spec has been adjusted
accordingly.

Issue #259

Change-Id: I3c8b65554096502577b48082c638c662eee98ac5
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4823
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast.go b/cue/ast.go
index c83397b..0e43a64 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -280,6 +280,9 @@
 
 			case *ast.Comprehension:
 				v1.walk(x)
+
+			case *ast.Attribute:
+				// Nothing to do.
 			}
 		}
 		if v.ctx().inDefinition > 0 && !obj.optionals.isFull() {
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index b95714c..4138969 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -282,6 +282,7 @@
 	Text string // must be a valid attribute format.
 
 	comments
+	decl
 }
 
 func (a *Attribute) Pos() token.Pos  { return a.At }
diff --git a/cue/ast/astutil/resolve.go b/cue/ast/astutil/resolve.go
index af8cbd3..9195ca9 100644
--- a/cue/ast/astutil/resolve.go
+++ b/cue/ast/astutil/resolve.go
@@ -276,6 +276,10 @@
 	case *ast.ImportSpec:
 		return nil
 
+	case *ast.Attribute:
+		// TODO: tokenize attributes, resolve identifiers and store the ones
+		// that resolve in a list.
+
 	case *ast.SelectorExpr:
 		walk(s, x.X)
 		return nil
diff --git a/cue/attr.go b/cue/attr.go
index 636fa05..03ec8d3 100644
--- a/cue/attr.go
+++ b/cue/attr.go
@@ -63,7 +63,8 @@
 		}
 	}
 
-	sort.Slice(as, func(i, j int) bool { return as[i].text < as[j].text })
+	sort.SliceStable(as, func(i, j int) bool { return as[i].text < as[j].text })
+	// TODO: remove these restrictions.
 	for i := 1; i < len(as); i++ {
 		if ai, aj := as[i-1], as[i]; ai.key() == aj.key() {
 			n := newNode(attrs[0])
diff --git a/cue/attr_test.go b/cue/attr_test.go
index 5f1f7f0..c48ce96 100644
--- a/cue/attr_test.go
+++ b/cue/attr_test.go
@@ -146,7 +146,7 @@
 			attrs, err := createAttrs(&context{}, baseValue{}, a)
 
 			if tc.err != "" {
-				if !strings.Contains(debugStr(&context{}, err), tc.err) {
+				if err == nil || !strings.Contains(debugStr(&context{}, err), tc.err) {
 					t.Errorf("error was %v; want %v", err, tc.err)
 				}
 				return
diff --git a/cue/format/node.go b/cue/format/node.go
index 86309f4..3972a49 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -352,6 +352,9 @@
 		f.expr(n.Expr)
 		f.print(declcomma) // implied
 
+	case *ast.Attribute:
+		f.print(n.At, n)
+
 	case *ast.CommentGroup:
 		f.print(newsection)
 		f.printComment(n)
diff --git a/cue/format/testdata/simplify.golden b/cue/format/testdata/simplify.golden
index 929063b..53f7a18 100644
--- a/cue/format/testdata/simplify.golden
+++ b/cue/format/testdata/simplify.golden
@@ -6,6 +6,8 @@
 
 "a.b": "foo-": cc_dd: x
 
+@attr(3)
+
 a: b: c: 3
 
 // references to bar are all shadowed and this can be safely turned into
@@ -26,10 +28,13 @@
 "_foo": 3
 
 x: {
+	@tag0(foo)
 	r1: baz1
 	bar: r2: bar
-	r3:           bar
-	E=quux:       3
+	r3:     bar
+	E=quux: 3
+
+	@tag1(bar)
 	r4:           quux
 	[baz2="str"]: 4
 	r5:           baz2
diff --git a/cue/format/testdata/simplify.input b/cue/format/testdata/simplify.input
index 4bb466a..d642ae1 100644
--- a/cue/format/testdata/simplify.input
+++ b/cue/format/testdata/simplify.input
@@ -6,6 +6,7 @@
 
 "a.b": "foo-": "cc_dd": x
 
+@attr(3)
 
 a:
     b:
@@ -29,10 +30,13 @@
 "_foo": 3
 
 x: {
+@tag0(foo)
     r1: baz1
     bar: r2: bar
     r3: bar
     E=quux: 3
+
+        @tag1(bar)
     r4: quux
     [baz2="str"]: 4
     r5: baz2
diff --git a/cue/load/import_test.go b/cue/load/import_test.go
index 294de7f..96984ad 100644
--- a/cue/load/import_test.go
+++ b/cue/load/import_test.go
@@ -60,7 +60,7 @@
 	var e *NoFilesError
 	ok := xerrors.As(err, &e)
 	if !ok {
-		t.Fatal(`Import("testdata/ignored") did not return NoCUEError.`)
+		t.Fatal(`Import("testdata/ignored") did not return NoFilesError.`)
 	}
 	if !e.ignored {
 		t.Fatal(`Import("testdata/ignored") should have ignored CUE files.`)
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 7ef6ef1..177513d 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -684,9 +684,17 @@
 		case token.FOR, token.IF, token.LET:
 			list = append(list, p.parseComprehension())
 
+		case token.ATTRIBUTE:
+			list = append(list, p.parseAttribute())
+			if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF
+				p.next()
+			}
+
 		default:
 			list = append(list, p.parseField())
 		}
+
+		// TODO: handle next comma here, after disallowing non-colon separator.
 	}
 
 	if p.tok == token.ELLIPSIS {
@@ -948,16 +956,30 @@
 func (p *parser) parseAttributes() (attrs []*ast.Attribute) {
 	p.openList()
 	for p.tok == token.ATTRIBUTE {
-		c := p.openComments()
-		a := &ast.Attribute{At: p.pos, Text: p.lit}
-		p.next()
-		c.closeNode(p, a)
-		attrs = append(attrs, a)
+		attrs = append(attrs, p.parseAttribute())
 	}
 	p.closeList()
 	return attrs
 }
 
+func (p *parser) parseAttributeDecls() (a []ast.Decl) {
+	for p.tok == token.ATTRIBUTE {
+		a = append(a, p.parseAttribute())
+		if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF
+			p.next()
+		}
+	}
+	return a
+}
+
+func (p *parser) parseAttribute() *ast.Attribute {
+	c := p.openComments()
+	a := &ast.Attribute{At: p.pos, Text: p.lit}
+	p.next()
+	c.closeNode(p, a)
+	return a
+}
+
 func (p *parser) parseLabel(rhs bool) (label ast.Label, expr ast.Expr, ok bool) {
 	tok := p.tok
 	switch tok {
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index e5eb5b6..1082dbc 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -471,6 +471,24 @@
 		import . "foo"
 		`,
 		out: "import , \"foo\"\nexpected 'STRING', found '.'",
+	}, {
+		desc: "attributes",
+		in: `
+		package name
+
+		@t1(v1)
+
+		{
+			@t2(v2)
+		}
+		a: {
+			a: 1
+			@t3(v3)
+			@t4(v4)
+			c: 2
+		}
+		`,
+		out: "package name, @t1(v1), {@t2(v2)}, a: {a: 1, @t3(v3), @t4(v4), c: 2}",
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index a9e207b..c1fc803 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -2842,7 +2842,7 @@
 to a data format
 
 ```
-SourceFile      = { attribute "," } [ PackageClause "," ] { ImportDecl "," } { Declaration "," } .
+SourceFile      = [ PackageClause "," ] { ImportDecl "," } { Declaration "," } .
 ```
 
 ```