cue/parser: parse new comprehension syntax

Change-Id: I3e75bc9abb45c45b25889675b72504377c28f1e1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3180
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast_test.go b/cue/ast_test.go
index 0719f1b..03cdbe4 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -257,7 +257,7 @@
 		in: `
 			a: int @b('' ,b) // invalid
 		`,
-		out: "attribute missing ')':\n    test:2:16\nmissing ',' in file:\n    test:3:3\n<0>{}",
+		out: "attribute missing ')':\n    test:2:16\nmissing ',' in struct literal:\n    test:3:3\n<0>{}",
 	}}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
diff --git a/cue/parser/error_test.go b/cue/parser/error_test.go
index 887d855..d25dbc0 100644
--- a/cue/parser/error_test.go
+++ b/cue/parser/error_test.go
@@ -177,9 +177,11 @@
 	}
 	for _, fi := range list {
 		name := fi.Name()
-		if !fi.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".src") {
-			checkErrors(t, filepath.Join(testdata, name), nil)
-		}
+		t.Run(name, func(t *testing.T) {
+			if !fi.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".src") {
+				checkErrors(t, filepath.Join(testdata, name), nil)
+			}
+		})
 	}
 }
 
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 7a6b9ef..e03341d 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -646,7 +646,13 @@
 	defer p.closeList()
 
 	for p.tok != token.RBRACE && p.tok != token.ELLIPSIS && p.tok != token.EOF {
-		list = append(list, p.parseField())
+		switch p.tok {
+		case token.FOR, token.IF, token.LET:
+			list = append(list, p.parseComprehension())
+
+		default:
+			list = append(list, p.parseField())
+		}
 	}
 
 	if p.tok == token.ELLIPSIS {
@@ -656,6 +662,70 @@
 	return
 }
 
+func (p *parser) parseComprehension() (decl ast.Decl) {
+	if p.trace {
+		defer un(trace(p, "Field"))
+	}
+
+	c := p.openComments()
+	defer func() { c.closeNode(p, decl) }()
+
+	tok := p.tok
+	pos := p.pos
+	clauses, fc := p.parseComprehensionClauses(true)
+	if fc != nil {
+		lab := &ast.Ident{
+			NamePos: pos,
+			Name:    tok.String(),
+		}
+		opt := token.NoPos
+		tok = p.tok
+		pos = p.pos
+		if p.tok == token.OPTION {
+			opt = pos
+			p.next()
+			tok = p.tok
+			pos = p.pos
+		}
+		p.next()
+		rhs := p.parseRHS()
+
+		// Field or alias.
+		switch tok {
+		case token.BIND:
+			decl = &ast.Alias{Ident: lab, Equal: pos, Expr: rhs}
+		case token.ISA, token.COLON:
+			decl = &ast.Field{
+				Label:    lab,
+				Optional: opt,
+				TokenPos: pos,
+				Token:    tok,
+				Value:    rhs,
+				Attrs:    p.parseAttributes(),
+			}
+		default:
+			decl = &ast.BadDecl{From: lab.Pos(), To: rhs.End()}
+		}
+		fc.closeNode(p, decl)
+		if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF
+			p.next()
+		}
+
+		return decl
+	}
+
+	expr := p.parseStruct()
+
+	if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF
+		p.next()
+	}
+
+	return &ast.Comprehension{
+		Clauses: clauses,
+		Value:   expr,
+	}
+}
+
 func (p *parser) parseField() (decl ast.Decl) {
 	if p.trace {
 		defer un(trace(p, "Field"))
@@ -682,7 +752,7 @@
 				expr = p.parseExpr()
 			}
 			e := &ast.EmbedDecl{Expr: expr}
-			if p.atComma("file", token.RBRACE) {
+			if p.atComma("struct literal", token.RBRACE) {
 				p.next()
 			}
 			return e
@@ -755,16 +825,10 @@
 	p.next() // : or ::
 	m.Value = p.parseRHS()
 
-	p.openList()
-	for p.tok == token.ATTRIBUTE {
+	if attrs := p.parseAttributes(); attrs != nil {
 		allowComprehension = false
-		c := p.openComments()
-		a := &ast.Attribute{At: p.pos, Text: p.lit}
-		p.next()
-		c.closeNode(p, a)
-		m.Attrs = append(m.Attrs, a)
+		m.Attrs = attrs
 	}
-	p.closeList()
 
 	decl = this
 	switch p.tok {
@@ -776,7 +840,7 @@
 		if !allowComprehension {
 			p.errf(p.pos, "comprehension not allowed for this field")
 		}
-		clauses := p.parseComprehensionClauses()
+		clauses, _ := p.parseComprehensionClauses(false)
 		return &ast.Comprehension{
 			Clauses: clauses,
 			Value: &ast.StructLit{
@@ -792,6 +856,19 @@
 	return decl
 }
 
+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)
+	}
+	p.closeList()
+	return attrs
+}
+
 func (p *parser) parseLabel(f *ast.Field) (expr ast.Expr, ok bool) {
 	switch p.tok {
 	case token.IDENT:
@@ -886,17 +963,26 @@
 	return elts
 }
 
-func (p *parser) parseComprehensionClauses() (clauses []ast.Clause) {
+// parseComprehensionClauses parses either new-style (first==true)
+// or old-style (first==false).
+// Should we now disallow keywords as identifiers? If not, we need to
+// return a list of discovered labels as the alternative.
+func (p *parser) parseComprehensionClauses(first bool) (clauses []ast.Clause, c *commentState) {
 	// TODO: reuse Template spec, which is possible if it doesn't check the
 	// first is an identifier.
+
 	for {
-		if p.tok == token.COMMA {
-			p.next()
-		}
 		switch p.tok {
 		case token.FOR:
 			c := p.openComments()
 			forPos := p.expect(token.FOR)
+			if first {
+				switch p.tok {
+				case token.COLON, token.ISA, token.BIND, token.OPTION:
+					return nil, c
+				}
+			}
+
 			var key, value *ast.Ident
 			var colon token.Pos
 			value = p.parseIdent()
@@ -918,15 +1004,28 @@
 
 		case token.IF:
 			c := p.openComments()
+			ifPos := p.expect(token.IF)
+			if first {
+				switch p.tok {
+				case token.COLON, token.ISA, token.BIND, token.OPTION:
+					return nil, c
+				}
+			}
+
 			clauses = append(clauses, c.closeClause(p, &ast.IfClause{
-				If:        p.expect(token.IF),
+				If:        ifPos,
 				Condition: p.parseExpr(),
 			}))
 
-		// TODO: case LET:
+		// case token.LET:
 		default:
-			return clauses
+			return clauses, nil
 		}
+		if p.tok == token.COMMA {
+			p.next()
+		}
+
+		first = false
 	}
 }
 
@@ -942,7 +1041,7 @@
 
 	elts := p.parseListElements()
 
-	if clauses := p.parseComprehensionClauses(); clauses != nil {
+	if clauses, _ := p.parseComprehensionClauses(false); clauses != nil {
 		var expr ast.Expr
 		if len(elts) != 1 {
 			p.errf(lbrack.Add(1), "list comprehension must have exactly one element")
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index 38f4114..ef26627 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -229,7 +229,11 @@
 		"field comprehensions",
 		`{
 				y: { a: 1, b: 2}
-				a: { "\(k)": v for k, v in y if v > 2 }
+				a: {
+					for k, v in y if v > 2 {
+						"\(k)": v
+					}
+				}
 			 }`,
 		`{y: {a: 1, b: 2}, a: {for k: v in y if v>2 {"\(k)": v}}}`,
 	}, {