cue/parser: allow let clause in comprehension

Closes #284

Change-Id: Ie051eb90f98f8e3cdf85d2aa3177ca6355619c9b
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7303
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/ast/astutil/resolve.go b/cue/ast/astutil/resolve.go
index 9ddd8f7..00f33c9 100644
--- a/cue/ast/astutil/resolve.go
+++ b/cue/ast/astutil/resolve.go
@@ -413,20 +413,30 @@
 
 func scopeClauses(s *scope, clauses []ast.Clause) *scope {
 	for _, c := range clauses {
-		if f, ok := c.(*ast.ForClause); ok { // TODO(let): support let clause
-			walk(s, f.Source)
-			s = newScope(s.file, s, f, nil)
-			if f.Key != nil {
-				name, err := ast.ParseIdent(f.Key)
+		switch x := c.(type) {
+		case *ast.ForClause:
+			walk(s, x.Source)
+			s = newScope(s.file, s, x, nil)
+			if x.Key != nil {
+				name, err := ast.ParseIdent(x.Key)
 				if err == nil {
-					s.insert(name, f.Key, f)
+					s.insert(name, x.Key, x)
 				}
 			}
-			name, err := ast.ParseIdent(f.Value)
+			name, err := ast.ParseIdent(x.Value)
 			if err == nil {
-				s.insert(name, f.Value, f)
+				s.insert(name, x.Value, x)
 			}
-		} else {
+
+		case *ast.LetClause:
+			walk(s, x.Expr)
+			s = newScope(s.file, s, x, nil)
+			name, err := ast.ParseIdent(x.Ident)
+			if err == nil {
+				s.insert(name, x.Ident, x)
+			}
+
+		default:
 			walk(s, c)
 		}
 	}
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 92143f1..b0b1431 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -1123,11 +1123,20 @@
 				Condition: p.parseRHS(),
 			}))
 
-		// TODO:
-		// case token.LET:
-		// 	c := p.openComments()
-		// 	p.expect(token.LET)
-		// 	return nil, c
+		case token.LET:
+			c := p.openComments()
+			letPos := p.expect(token.LET)
+
+			ident := p.parseIdent()
+			assign := p.expect(token.BIND)
+			expr := p.parseRHS()
+
+			clauses = append(clauses, c.closeClause(p, &ast.LetClause{
+				Let:   letPos,
+				Ident: ident,
+				Equal: assign,
+				Expr:  expr,
+			}))
 
 		default:
 			return clauses, nil
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index 15b1646..a829192 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -280,6 +280,17 @@
 			 }`,
 		`{y: {a: 1, b: 2}, a: {for k: v in y if v>2 {"\(k)": v}}}`,
 	}, {
+		"nested comprehensions",
+		`{
+			y: { a: 1, b: 2}
+			a: {
+				for k, v in y let x = v+2 if x > 2 {
+					"\(k)": v
+				}
+			}
+		}`,
+		`{y: {a: 1, b: 2}, a: {for k: v in y let x=v+2 if x>2 {"\(k)": v}}}`,
+	}, {
 		"let declaration",
 		`{
 			let X = 42
diff --git a/cue/testdata/eval/comprehensions.txtar b/cue/testdata/eval/comprehensions.txtar
index 09152da..a0da3af 100644
--- a/cue/testdata/eval/comprehensions.txtar
+++ b/cue/testdata/eval/comprehensions.txtar
@@ -13,6 +13,12 @@
 		l: 40
 	}
 }
+
+c: {
+  for k, v in a let y = v+10 if y > 50 {
+    "\(k)": y
+  }
+}
 -- out/eval --
 (struct){
   a: (struct){
@@ -26,6 +32,10 @@
     z: (int){ 50 }
     l: (int){ 40 }
   }
+  c: (struct){
+    y: (int){ 110 }
+    z: (int){ 60 }
+  }
 }
 -- out/compile --
 --- in.cue
@@ -48,4 +58,9 @@
       l: 40
     }
   }
+  c: {
+    for k, v in 〈1;a〉 let y = (〈0;v〉 + 10) if (〈0;y〉 > 50) {
+      "\(〈2;k〉)": 〈1;y〉
+    }
+  }
 }
diff --git a/internal/core/subsume/structural.go b/internal/core/subsume/structural.go
index bc581cb..7f1725d 100644
--- a/internal/core/subsume/structural.go
+++ b/internal/core/subsume/structural.go
@@ -275,6 +275,8 @@
 			c.yielders = append(c.yielders, x)
 
 		case *adt.LetClause:
+			c.yielders = append(c.yielders, x)
+
 		case *adt.ValueClause:
 		}
 	}